Commit: e7f3c3137a4c543223b59f61e41b2f5dedbb7bb0
Author: gwoo | Date: 2009-10-12 19:46:17 -0700
diff --git a/.gitignore b/.gitignore
index e69de29..ad2c1cd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+app/libraries/plugins/*
\ No newline at end of file
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 0000000..417c613
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,5 @@
+<IfModule mod_rewrite.c>
+ RewriteEngine on
+ RewriteRule ^$ app/webroot/ [L]
+ RewriteRule (.*) app/webroot/$1 [L]
+</IfModule>
diff --git a/app/.htaccess b/app/.htaccess
new file mode 100644
index 0000000..0ed8662
--- /dev/null
+++ b/app/.htaccess
@@ -0,0 +1,5 @@
+<IfModule mod_rewrite.c>
+ RewriteEngine on
+ RewriteRule ^$ webroot/ [L]
+ RewriteRule (.*) webroot/$1 [L]
+ </IfModule>
\ No newline at end of file
diff --git a/app/config/bootstrap.php b/app/config/bootstrap.php
new file mode 100644
index 0000000..06c130b
--- /dev/null
+++ b/app/config/bootstrap.php
@@ -0,0 +1,208 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium;
+
+use \lithium\core\Environment;
+use \lithium\core\Libraries;
+
+/**
+ * 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(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.
+ */
+define('LITHIUM_APP_PATH', dirname(__DIR__));
+
+/**
+ * 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 .= "config/bootstrap.php. It should point to the directory containing your ";
+ $message .= "/libraries directory.";
+ trigger_error($message, E_USER_ERROR);
+}
+
+/**
+ * Add the Lithium core library. This sets default paths and initializes the autoloader. You
+ * generally should not need to override any settings.
+ */
+Libraries::add('lithium');
+
+/**
+ * Optimize default request cycle by loading common classes. If you're implementing custom
+ * request/response or dispatch classes, you can safely remove these. Actually, you can safely
+ * remove them anyway, they're just there to give slightly you better out-of-the-box performance.
+ */
+require LITHIUM_LIBRARY_PATH . '/lithium/core/Object.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/core/StaticObject.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/util/Collection.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/util/collection/Filters.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/util/Inflector.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/util/Set.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/util/String.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/core/Environment.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/http/Base.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/http/Media.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/http/Request.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/http/Response.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/http/Route.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/action/Controller.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/action/Dispatcher.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/action/Request.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/action/Response.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/template/View.php';
+require LITHIUM_LIBRARY_PATH . '/lithium/template/view/Renderer.php';
+
+/**
+ * Add the application. You can pass a `'path'` key here if this bootstrap file is outside of
+ * your main application, but generally you should not need to change any settings.
+ */
+Libraries::add('app');
+
+/**
+ * Add some plugins
+ */
+// Libraries::add('plugin', 'docs');
+
+/**
+ * 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. When creating a
+ * new application, it is suggested that you change the value of `'key'` below.
+ */
+
+/**
+* Session configuration
+*/
+// use \lithium\storage\Session;
+//
+// Session::config(array(
+// 'cookie' => array(
+// 'adapter' => 'Cookie',
+// 'name' => 'AppCookieName',
+// 'expires' => '+5 days',
+// 'domain' => '',
+// 'path' => '/',
+// 'filters' => array(
+// // 'Encryption' => array('key' => '0409448a5206980ab15682c3281c1a3b1fb10c55')
+// )
+// ),
+// 'default' => array('adapter' => 'Php', 'filters' => array())
+// ));
+
+/**
+ * To enable admin or plugin routing, uncomment the following lines, and see `app/config/routes.php`
+ * to enable the admin routing namespace.
+ */
+// use \lithium\action\Dispatcher;
+//
+// Dispatcher::config(array('rules' => array(
+// 'admin' => array('action' => 'admin_{:action}'),
+// 'plugin' => array('controller' => '{:plugin}.{:controller}')
+// )));
+
+/**
+ * Uncomment to set globalization defaults. A locale consists of a language and
+ * an optional territory code i.e. `'en_US'` or `'en'`. For timezone specify
+ * a valid timezone identifier i.e. `'America/New_York'` or `'Etc/UTC'`. You may
+ * also specify additional sources for retrieving translated and messages and
+ * localized data or add rules, formats, messages or lists data right here.
+ */
+// use \lithium\g11n\G11n;
+//
+// G11n::locale('en');
+// G11n::timezone('Etc/UTC');
+// G11n::sources(LITHIUM_APP_PATH . '/extensions/g11n');
+// G11n::rules('plural', array('en' => function($n) { return $n != 1 ? 1 : 0; }));
+
+/*
+ * Inflector configuration example. 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.
+ */
+// use lithium\util\Inflector;
+//
+// Inflector::rules("plural", array(
+// '/(s)tatus$/i' => '\1\2tatuses',
+// '/^(ox)$/i' => '\1\2en',
+// '/([m|l])ouse$/i' => '\1ice'
+// ));
+//
+// Inflector::rules("uninflectedPlural", array('.*[nrlm]ese', '.*deer', '.*ois', '.*pox'));
+//
+// Inflector::rules("irregularPlural", array('atlas' => 'atlases', 'brother' => 'brothers'));
+//
+// Inflector::rules("singular", array(
+// '/(s)tatuses$/i' => '\1\2tatus',
+// '/(matr)ices$/i' =>'\1ix','/(vert|ind)ices$/i'
+// ));
+
+/**
+ * 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
+ * extracting message templates from source code).
+ *
+ * - `'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_ or
+ * _gettext_ adapter which handle this internally.
+ */
+// use lithium\g11n\Catalog;
+//
+// Catalog::config(array(
+// 'runtime' => array('adapter' => 'Memory'),
+// 'app' => array('adapter' => 'Gettext', 'path' => LITHIUM_APP_PATH . '/resources/po'),
+// 'lithium' => array('adapter' => 'Gettext', 'path' => LITHIUM_LIBRARY_PATH . '/lithium/resources/po')
+// ));
+
+/**
+ * Globalization runtime data. You can add globalized data during runtime utilizing a
+ * configuration set up to use the _memory_ adapter.
+ */
+// $data = array('en' => function($n) { return $n != 1 ? 1 : 0; });
+// Catalog::write('message.plural', $data, array('name' => 'runtime'));
+
+/**
+ * Enabling globalization integration. Classes in the framework are designed with
+ * globalization in mind. To enable globalization for these classes we just need to pass
+ * the needed data into them.
+ */
+// use lithium\util\Validator;
+// use lithium\util\Inflector;
+//
+// Validator::add('postalCode',
+// Catalog::read('validation.postalCode', array('en_US'))
+// );
+// Inflector::rules('transliterations',
+// Catalog::read('inflection.transliteration', array('en'))
+// );
+
+/**
+ * Your custom code goes here.
+ */
+
+?>
\ No newline at end of file
diff --git a/app/config/connections.php b/app/config/connections.php
new file mode 100644
index 0000000..3098089
--- /dev/null
+++ b/app/config/connections.php
@@ -0,0 +1,78 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use \lithium\data\Connections;
+
+/**
+ * Database configuration.
+ * You can specify multiple configurations for production, development and testing.
+ *
+ * adapter => The name of a supported driver; valid options are as follows:
+ * mysql - MySQL 4 & 5,
+ * mysqli - MySQL 4 & 5 Improved Interface (PHP5 only),
+ * sqlite - SQLite (PHP5 only),
+ * postgres - PostgreSQL 7 and higher,
+ * mssql - Microsoft SQL Server 2000 and higher,
+ * db2 - IBM DB2, Cloudscape, and Apache Derby (http://php.net/ibm-db2)
+ * oracle - Oracle 8 and higher
+ * firebird - Firebird/Interbase
+ * sybase - Sybase ASE
+ *
+ * You can add custom database drivers (or override existing drivers) by adding the
+ * appropriate file to app/models/datasources/dbo. Drivers should be named 'dbo_x.php',
+ * where 'x' is the name of the database.
+ *
+ * persistent => true / false
+ * Determines whether or not the database should use a persistent connection.
+ *
+ * host =>
+ * the host you connect to the database. To add a socket or port number, use 'port' => #
+ *
+ * prefix =>
+ * Uses the given prefix for all the tables in this database. This setting can be overridden
+ * on a per-table basis with the Model::$_meta['prefix'] property.
+ *
+ * schema =>
+ * For Postgres and DB2, specifies which schema you would like to use the tables in. Postgres
+ * defaults to 'public', DB2 defaults to empty.
+ *
+ * encoding =>
+ * For MySQL, MySQLi, Postgres and DB2, specifies the character encoding to use when connecting
+ * to the database. Defaults to 'UTF-8' for DB2. Uses database default for all others.
+ */
+Connections::add('default', 'Database', array(
+ // 'development' => array(
+ 'adapter' => 'MySql',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'database' => 'lithium-blog'
+ // 'adapter' => 'sqlite',
+ // 'database' => LITHIUM_APP_PATH . '/tmp/default.db'
+ // ),
+ // 'test' => array(
+ // 'adapter' => 'mysql',
+ // 'host' => 'localhost',
+ // 'login' => 'user',
+ // 'password' => 'password',
+ // 'database' => 'test_database_name'
+ // ),
+ // 'production' => array(
+ // 'adapter' => 'mysql',
+ // 'host' => 'localhost',
+ // 'login' => 'user',
+ // 'password' => 'password',
+ // 'database' => 'test_database_name'
+ // )
+));
+
+?>
\ No newline at end of file
diff --git a/app/config/environments.php b/app/config/environments.php
new file mode 100644
index 0000000..0bee466
--- /dev/null
+++ b/app/config/environments.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use \lithium\core\Libraries;
+use \lithium\core\Environment;
+
+/*
+ * Set up the "development" environment
+ */
+Environment::set("development", array(
+ "Output.varDump" => true, // Writes output from pr() and debug()
+ "Output.sqlDump" => true, // Writes SQL log at the bottom of the page
+ "Output.timestamp.enabled" => true, // Output the page generation time
+ "Output.timestamp.format" => "<!-- %01.4fs -->", // Page generation time output format string
+ "Cache.enabled" => true,
+ "Cache.expires" => "+10 seconds",
+ "Asset.compress" => false,
+ "Asset.timestamp" => true
+));
+
+/*
+ * Set the current environment to "development"
+ */
+
+Environment::set("development");
+
+/*
+ * Inflector configuration example
+ */
+// Inflector::add("plural", array(
+// '/(s)tatus$/i' => '\1\2tatuses', '/^(ox)$/i' => '\1\2en', '/([m|l])ouse$/i' => '\1ice'
+// ));
+// Inflector::add("uninflectedPlural", array(
+// '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox'
+// ));
+// Inflector::add("irregularPlural", array(
+// 'atlas' => 'atlases', 'beef' => 'beefs', 'brother' => 'brothers'
+// ));
+// Inflector::add("singular", array(
+// '/(s)tatuses$/i' => '\1\2tatus', '/(matr)ices$/i' =>'\1ix','/(vert|ind)ices$/i'
+// ));
+
+/*
+ * Paths configuration example
+ */
+// Libraries::addPluginPath("/path/to/more/plugins");
+
+?>
\ No newline at end of file
diff --git a/app/config/environments/development.php b/app/config/environments/development.php
new file mode 100644
index 0000000..2286810
--- /dev/null
+++ b/app/config/environments/development.php
@@ -0,0 +1,31 @@
+<?php
+
+use \lithium\core\Libraries;
+use \lithium\core\Environment;
+
+/*
+ * Set up the "development" environment
+ */
+Environment::set("development", array(
+ "Output.varDump" => true, // Writes output from pr() and debug()
+ "Output.sqlDump" => true, // Writes SQL log at the bottom of the page
+ "Output.timestamp.enabled" => true, // Output the page generation time
+ "Output.timestamp.format" => "<!-- %01.4fs -->", // Page generation time output format string
+ "Cache.enabled" => true,
+ "Cache.expires" => "+10 seconds",
+ "Asset.compress" => false,
+ "Asset.timestamp" => true,
+ // "G11n.locale" => "en",
+ // "G11n.timezone" => "Etc/UTC",
+ // "G11n.currency" => "USD"
+));
+
+/*
+ * Set the current environment to "development"
+ */
+// switch (true) {
+// case
+// }
+Environment::set("development");
+
+?>
\ No newline at end of file
diff --git a/app/config/routes.php b/app/config/routes.php
new file mode 100644
index 0000000..c439c5f
--- /dev/null
+++ b/app/config/routes.php
@@ -0,0 +1,40 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use \lithium\http\Router;
+
+/**
+ * Uncomment the line below to enable routing for admin actions.
+ * @todo Implement me.
+ */
+// Router::namespace('/admin', array('admin' => true));
+
+/**
+ * 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)...
+ */
+Router::connect('/', array('controller' => 'pages', 'action' => 'view', 'home'));
+
+/**
+ * ...and connect the rest of 'Pages' controller's urls.
+ */
+Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'view'));
+
+/**
+ * Finally, connect the default routes.
+ */
+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/config/schema/empty b/app/config/schema/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/config/switchboard.php b/app/config/switchboard.php
new file mode 100644
index 0000000..48ae07c
--- /dev/null
+++ b/app/config/switchboard.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * Welcome to the switchboard. This file contains a series of method filters that allow you to
+ * intercept different parts of Lithium's request cycle as they happen. You can apply filters to
+ * any object method that has a `@filter` flag in its API documentation.
+ *
+ * When applying a filter, 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 3 parameters: `$self`,
+ * `$params`, and `$chain`.
+ *
+ * - `$self`: If the filter is applied on an object instance, then `$self` will be that instance. If
+ * applied to a static class, then `$self` will be a string containing the fully-namespaced class
+ * name.
+ *
+ * - `$params`: Contains an associative array of the parameters that are passed into the method. You
+ * can modify or inspect these parameters before allowing the method to continue.
+ *
+ * - `$chain`: Finally, `$chain` contains the list of filters in line to be executed. At the bottom
+ * of `$chain` is the method itself. This is why most filters contain a line that looks like
+ * `return $chain->next($self, $params, $chain);`. This passes control to the next filter in the
+ * chain, and finally, to the method itself. This allows you to interact with the return value as
+ * well as the parameters.
+ */
+
+use \lithium\http\Router;
+use \lithium\core\Environment;
+use \lithium\action\Dispatcher;
+
+/**
+ * Loads application routes before the request is dispatched. Change this to `include_once` if
+ * more than one request cycle is executed per HTTP request.
+ *
+ * @see lithium\http\Router
+ */
+Dispatcher::applyFilter('run', function($self, $params, $chain) {
+ include __DIR__ . '/routes.php';
+ 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/controllers/HelloWorldController.php b/app/controllers/HelloWorldController.php
new file mode 100644
index 0000000..5c78979
--- /dev/null
+++ b/app/controllers/HelloWorldController.php
@@ -0,0 +1,22 @@
+<?php
+
+namespace app\controllers;
+
+class HelloWorldController extends \lithium\action\Controller {
+
+ public $helpers = array();
+
+ public function index() {
+ $this->render(array('layout' => false));
+ }
+
+ public function to_string() {
+ return "Hello World";
+ }
+
+ public function to_json() {
+ $this->render(array('json' => 'Hello World'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/app/controllers/PagesController.php b/app/controllers/PagesController.php
new file mode 100644
index 0000000..749aae1
--- /dev/null
+++ b/app/controllers/PagesController.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace App\Controllers;
+
+use \lithium\util\Inflector;
+
+class PagesController extends \lithium\action\Controller {
+
+ public $helpers = array('Html');
+
+ function view() {
+ $path = func_get_args();
+
+ if (!count($path)) {
+ $path = array('home');
+ }
+
+ $count = count($path);
+ $page = $subpage = $title = null;
+
+ $page = (!empty($path[0]) ? $path[0] : $page);
+ $subpage = (!empty($path[1]) ? $path[1] : $subpage);
+ $title = (!empty($path[$count - 1]) ? Inflector::humanize($path[$count - 1]) : $title);
+
+ $this->set(compact('page', 'subpage', 'title'));
+ $this->render(join('/', $path));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/app/extensions/adapters/empty b/app/extensions/adapters/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/extensions/behaviors/empty b/app/extensions/behaviors/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/extensions/commands/empty b/app/extensions/commands/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/extensions/components/empty b/app/extensions/components/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/extensions/data/sources/empty b/app/extensions/data/sources/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/extensions/g11n/empty b/app/extensions/g11n/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/extensions/helpers/empty b/app/extensions/helpers/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/index.php b/app/index.php
new file mode 100644
index 0000000..f19ecfe
--- /dev/null
+++ b/app/index.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+require 'webroot/index.php';
+
+?>
\ No newline at end of file
diff --git a/app/libraries/empty b/app/libraries/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/models/empty b/app/models/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/controllers/empty b/app/tests/cases/controllers/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/extensions/adapters/empty b/app/tests/cases/extensions/adapters/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/extensions/behaviors/empty b/app/tests/cases/extensions/behaviors/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/extensions/commands/empty b/app/tests/cases/extensions/commands/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/extensions/components/empty b/app/tests/cases/extensions/components/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/extensions/data_sources/empty b/app/tests/cases/extensions/data_sources/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/extensions/helpers/empty b/app/tests/cases/extensions/helpers/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/cases/models/empty b/app/tests/cases/models/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/fixtures/empty b/app/tests/fixtures/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tests/groups/empty b/app/tests/groups/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tmp/cache/empty b/app/tmp/cache/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/tmp/logs/empty b/app/tmp/logs/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/views/elements/empty b/app/views/elements/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/views/hello_world/index.html.php b/app/views/hello_world/index.html.php
new file mode 100644
index 0000000..c57eff5
--- /dev/null
+++ b/app/views/hello_world/index.html.php
@@ -0,0 +1 @@
+Hello World!
\ No newline at end of file
diff --git a/app/views/layouts/default.ajax.php b/app/views/layouts/default.ajax.php
new file mode 100644
index 0000000..f0aa163
--- /dev/null
+++ b/app/views/layouts/default.ajax.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+echo $content_for_layout;
+
+?>
\ No newline at end of file
diff --git a/app/views/layouts/default.email.html.php b/app/views/layouts/default.email.html.php
new file mode 100644
index 0000000..8968544
--- /dev/null
+++ b/app/views/layouts/default.email.html.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+?>
+<!doctype html>
+
+<html>
+<head>
+ <title><?=$this->title; ?></title>
+</head>
+<body>
+ <?=@$this->content; ?>
+</body>
+</html>
\ No newline at end of file
diff --git a/app/views/layouts/default.email.txt.php b/app/views/layouts/default.email.txt.php
new file mode 100644
index 0000000..f0aa163
--- /dev/null
+++ b/app/views/layouts/default.email.txt.php
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+echo $content_for_layout;
+
+?>
\ No newline at end of file
diff --git a/app/views/layouts/default.html.php b/app/views/layouts/default.html.php
new file mode 100644
index 0000000..23b565d
--- /dev/null
+++ b/app/views/layouts/default.html.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+?>
+<!doctype html>
+<html>
+<head>
+ <?=@$this->html->charset(); ?>
+ <title><?=$title; ?></title>
+ <?=@$this->html->style('base'); ?>
+ <?=@$this->scripts(); ?>
+ <?=@$this->html->link('Icon', null, array('type' => 'icon')); ?>
+</head>
+<body>
+ <div id="container">
+ <div id="header"></div>
+ <div id="content">
+ <?=@$this->content; ?>
+ </div>
+ <div id="footer"></div>
+ </div>
+</body>
+</html>
diff --git a/app/views/layouts/default.xml.php b/app/views/layouts/default.xml.php
new file mode 100644
index 0000000..23f0d3e
--- /dev/null
+++ b/app/views/layouts/default.xml.php
@@ -0,0 +1,14 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+?>
+<?=@$xml->header(); ?>
+<?=@$content_for_layout; ?>
\ No newline at end of file
diff --git a/app/views/layouts/flash.html.php b/app/views/layouts/flash.html.php
new file mode 100644
index 0000000..5931844
--- /dev/null
+++ b/app/views/layouts/flash.html.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use \lithium\core\Environment;
+
+?>
+<!doctype html>
+<html>
+<head>
+ <?php echo $this->html->charset(); ?>
+ <title><?php echo $page_title; ?></title>
+ <?php if (Environment::is('production')) { ?>
+ <meta http-equiv="Refresh" content="<?=$pause?>;url=<?=$url?>"/>
+ <?php } ?>
+ <style>
+ p { text-align:center; font:bold 1.1em sans-serif }
+ a { color:#444; text-decoration: none }
+ a:hover { text-decoration: underline; color: #44E }
+ </style>
+</head>
+<body>
+ <p><a href="<?=$url; ?>"><?=$message; ?></a></p>
+</body>
+</html>
\ No newline at end of file
diff --git a/app/views/pages/home.html.php b/app/views/pages/home.html.php
new file mode 100644
index 0000000..dd87c77
--- /dev/null
+++ b/app/views/pages/home.html.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+?>
+<h2>Lithium is coming...</h2>
+
+<p>
+ temporary home page that will eventually be filled with configuration checks.
+</p>
+
+<p>
+ <a href="http://rad-dev.org/lithium/wiki">Lithium Wiki</a>
+</p>
+<p>
+ <a href="http://rad-dev.org/lithium3">Lithium Source</a>
+</p>
+<p>
+ <a href="http://groups.google.com/group/lithium">Lithium Google Group</a>
+</p>
diff --git a/app/webroot/.htaccess b/app/webroot/.htaccess
new file mode 100644
index 0000000..21796cf
--- /dev/null
+++ b/app/webroot/.htaccess
@@ -0,0 +1,7 @@
+<IfModule mod_rewrite.c>
+ RewriteEngine On
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteCond %{REQUEST_FILENAME} !favicon.ico$
+ RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
+</IfModule>
\ No newline at end of file
diff --git a/app/webroot/css/base.css b/app/webroot/css/base.css
new file mode 100644
index 0000000..594a220
--- /dev/null
+++ b/app/webroot/css/base.css
@@ -0,0 +1,33 @@
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+* {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ background-color: #CCC;
+ font-family: Helvetica, Arial, sans-serif;
+ margin: 2em;
+}
+
+a {
+ color: #333;
+}
+
+a:hover {
+ text-decoration: none;
+}
+
+p {
+ margin-top: 1em;
+}
\ No newline at end of file
diff --git a/app/webroot/css/debug.css b/app/webroot/css/debug.css
new file mode 100644
index 0000000..6dbfbd9
--- /dev/null
+++ b/app/webroot/css/debug.css
@@ -0,0 +1,281 @@
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+h1.test-dashboard {
+ padding: 8px 0 4px 14px;
+ background: #B5B4A4;
+}
+
+/*--- Benchmarking ---*/
+table.metrics {
+ border: 0;
+ border-top: 1px solid #ccc;
+}
+
+td.metric-name {
+ text-align: left;
+ white-space: nowrap;
+ padding: 6px 8px;
+ background: #f4f4f4;
+ width: 35%;
+}
+td.metric {
+ border: 0;
+ font-family: 'Courier New', Courier;
+ font-weight: bold;
+ padding: 6px 8px;
+ text-align: right;
+ width: 65%;
+}
+
+ul.classes, ul.files {
+ list-style-type: none;
+ font-family: 'Andale Mono';
+}
+
+/*--- Test Results ---*/
+div.test-result {
+ margin: 0 0 15px 0;
+ padding: 8px 10px;
+ color: #FFFFFF;
+ border: 2px solid #000000;
+ font-family: Helvetica, Arial, sans-serif;
+ font-weight: bold;
+ font-size: 16px;
+}
+
+div.test-result-success {
+ background-color: #33CC66;
+ border-color: #009933;
+}
+
+div.test-result-fail {
+ background-color: #CC0033;
+ border-color: #990000;
+}
+
+div.test-assert, div.test-exception {
+ margin: 4px 0;
+ padding: 4px 8px;
+ color: #000000;
+ border: 1px solid #000000;
+ font-family: 'Monaco', 'Andale Mono', Helvetica, Arial, sans-serif;
+ line-height: 20px;
+ font-size: 12px;
+}
+
+div.test-assert-passed {
+ border-color: #339966;
+ background-color: #D0F9E0;
+}
+
+div.test-assert-failed, div.test-exception {
+ border-color: #993366;
+ background-color: #F9D0E0;
+}
+
+div.test-assert span.content, div.test-exception span.content, div.test-exception span.trace {
+ display: block;
+ clear: both;
+ white-space: pre;
+}
+
+div.test-exception span.content {
+ font-style: italic;
+}
+
+div.test-exception span.trace {
+ padding: 0 0 0 5px;
+ margin: 2px 0 2px 3px;
+ border-left: 1px solid #C09090;
+}
+
+/*--- SQL Dumps ---*/
+.lithium-sql-log table {
+ background: #f4f4f4;
+}
+
+.lithium-sql-log td {
+ padding: 4px 8px;
+ text-align: left;
+}
+
+
+/*--- Debugger Dumps ---*/
+pre {
+ color: #000;
+ background: #f0f0f0;
+ padding: 1em;
+}
+
+pre.lithium-debug {
+ background: #ffcc00;
+ font-size: 120%;
+ line-height: 140%;
+ margin-top: 1em;
+ overflow: auto;
+ position: relative;
+}
+
+div.lithium-stack-trace {
+ background: #fff;
+ border: 4px dotted #ffcc00;
+ color: #333;
+ margin: 0px;
+ padding: 6px;
+ font-size: 120%;
+ line-height: 140%;
+ overflow: auto;
+ position: relative;
+}
+
+/*--- Code Highlighting ---*/
+div.lithium-code-dump pre {
+ position: relative;
+ overflow: auto;
+}
+
+div.lithium-stack-trace pre, div.lithium-code-dump pre {
+ color: #000;
+ background-color: #F0F0F0;
+ margin: 0px;
+ padding: 1em;
+ overflow: auto;
+}
+
+div.lithium-code-dump pre, div.lithium-code-dump pre code {
+ clear: both;
+ font-size: 12px;
+ line-height: 15px;
+ margin: 4px 2px;
+ padding: 4px;
+ overflow: auto;
+}
+
+div.lithium-code-dump span.code-highlight {
+ background-color: #ff0;
+ padding: 4px;
+}
+
+/*--- Code Coverage Analysis ---*/
+span.filters {
+ float: right;
+ margin-top: -17px;
+}
+
+div.code-coverage-results {
+ color: #000000;
+ font-size: 11px;
+ font-family: 'Andale Mono';
+ background-color: #F0F0F0;
+ border: 1px solid #CCCCCC;
+}
+
+h4.coverage {
+ color: #000000;
+ background-color: #FFFFFF;
+ font-family: Helvetica, Arial;
+ font-weight: bold;
+ margin: 6px 0 3px 3px;
+ padding: 0;
+}
+
+div.code-coverage-results h4.name {
+ color: #666;
+ background-color: #F0F0F0;
+ border-bottom: 1px solid #999;
+ padding: 3px 0;
+ font-size: 12px;
+}
+
+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;
+}
+
+div.code-coverage-results div.uncovered span.content {
+ color: #B00;
+ background-color: #FEE;
+}
+
+div.code-coverage-results div.covered span.content {
+ color: #080;
+ background-color: #DFD;
+}
+
+div.code-coverage-results div.ignored span.content {
+ color: #aaa;
+}
+
+div.code-coverage-results span.line-num {
+ display: block;
+ float: left;
+ font-family: Helvetica, Arial, sans-serif;
+ width: 20px;
+ color: #A9A9A9;
+ text-align: right;
+ background-color: #ECECEC;
+ border-right: 1px solid #DDDDDD;
+ padding-right: 2px;
+ margin-right: 5px;
+ line-height: 20px;
+}
+
+div.code-coverage-results span.line-num strong {
+ color: #666;
+}
+
+div.code-coverage-results div.start {
+ margin-top: 30px;
+ padding-top: 5px;
+ border: 1px solid #aaa;
+ border-width: 1px 1px 0px 1px;
+}
+
+div.code-coverage-results div.end {
+ margin-bottom: 30px;
+ padding-bottom: 5px;
+ border: 1px solid #aaa;
+ border-width: 0px 1px 1px 1px;
+}
+
+div.code-coverage-results div.realstart {
+ margin-top: 0px;
+}
+
+div.code-coverage-results p.note {
+ color: #bbb;
+ padding: 5px;
+ margin: 5px 0 10px;
+ font-size: 10px;
+}
+
+div.code-coverage-results span.result-bad {
+ color: #a00;
+}
+
+div.code-coverage-results span.result-ok {
+ color: #fa0;
+}
+
+div.code-coverage-results span.result-good {
+ color: #0a0;
+}
diff --git a/app/webroot/favicon.ico b/app/webroot/favicon.ico
new file mode 100644
index 0000000..26ac224
Binary files /dev/null and b/app/webroot/favicon.ico differ
diff --git a/app/webroot/img/empty b/app/webroot/img/empty
new file mode 100644
index 0000000..e69de29
diff --git a/app/webroot/index.php b/app/webroot/index.php
new file mode 100644
index 0000000..b9ed30e
--- /dev/null
+++ b/app/webroot/index.php
@@ -0,0 +1,28 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+/**
+ * Welcome to Lithium 3! 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.
+ *
+ * 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
+ * app/config/bootstrap.php, which is loaded below:
+ */
+require dirname(__DIR__) . '/config/bootstrap.php';
+
+/**
+ * Dispatch a new request with the default settings.
+ */
+echo lithium\action\Dispatcher::run();
+
+?>
\ No newline at end of file
diff --git a/app/webroot/js/empty b/app/webroot/js/empty
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/app/webroot/js/empty
@@ -0,0 +1 @@
+
diff --git a/app/webroot/test.php b/app/webroot/test.php
new file mode 100644
index 0000000..c884dab
--- /dev/null
+++ b/app/webroot/test.php
@@ -0,0 +1,139 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+use \lithium\core\Libraries;
+use \lithium\test\Group;
+use \lithium\test\Dispatcher;
+use \lithium\util\Inflector;
+use \lithium\util\reflection\Inspector;
+
+$startBenchmark = microtime(true);
+
+error_reporting(E_ALL | E_STRICT | E_DEPRECATED);
+
+require dirname(__DIR__) . '/config/bootstrap.php';
+$core = dirname(dirname(__DIR__)) . '/libraries/lithium';
+
+$testRun = Dispatcher::run(null, $_GET);
+$stats = Dispatcher::process($testRun['results']);
+
+?>
+<!doctype html>
+<html>
+ <head>
+ <title>Lithium3 Unit Test Dashboard</title>
+ <link rel="stylesheet" href="css/base.css" />
+ <link rel="stylesheet" href="css/debug.css" />
+ </head>
+ <body>
+ <h1 class="test-dashboard">Lithium3 Unit Test Dashboard</h1>
+
+ <div style="float: left; padding: 10px 0 20px 20px; width: 20%;">
+ <h2><a href="?group=\">Tests</a></h2>
+ <?php echo Dispatcher::menu('html'); ?>
+ </div>
+
+ <div style="float:left; padding: 10px; width: 75%">
+ <h2>Stats for <?php echo $testRun['title']; ?></h2>
+
+ <h3>Test results</h3>
+
+ <span class="filters">
+ <?php
+ $filters = Libraries::locate('testFilters');
+ $base = $_SERVER['REQUEST_URI'];
+
+ foreach ($filters as $i => $class) {
+ $url = $base . "&filters[]={$class}";
+ $name = join('', array_slice(explode("\\", $class), -1));
+ $key = Inflector::underscore($name);
+
+ echo "<a class=\"{$key}\" href=\"{$url}\">{$name}</a>";
+
+ if ($i < count($filters) - 1) {
+ echo ' | ';
+ }
+ }
+ ?>
+ </span>
+
+ <?php
+ $passes = count($stats['passes']);
+ $fails = count($stats['fails']);
+ $errors = count($stats['errors']);
+ $exceptions = count($stats['exceptions']);
+ $success = ($passes === $stats['asserts'] && $errors === 0);
+
+ echo '<div class="test-result test-result-' . ($success ? 'success' : 'fail') . '"';
+ echo ">{$passes} / {$stats['asserts']} passes, {$fails} ";
+ echo ((intval($stats['fails']) == 1) ? 'fail' : 'fails') . " and {$exceptions} ";
+ echo ((intval($exceptions) == 1) ? 'exceptions' : 'exceptions');
+ echo '</div>';
+
+ foreach ((array)$stats['errors'] as $error) {
+ switch ($error['result']) {
+ case 'fail':
+ $error += array('class' => 'unknown', 'method' => 'unknown');
+ echo '<div class="test-assert test-assert-failed">';
+ echo "Assertion '{$error['assertion']}' failed in ";
+ echo "{$error['class']}::{$error['method']}() on line ";
+ echo "{$error['line']}: ";
+ echo "<span class=\"content\">{$error['message']}</span>";
+ break;
+ case 'exception':
+ echo '<div class="test-exception">';
+ echo "Exception thrown in {$error['class']}::{$error['method']}() ";
+ echo "on line {$error['line']}: ";
+ echo "<span class=\"content\">{$error['message']}</span>";
+ if (isset($error['trace']) && !empty($error['trace'])) {
+ echo "Trace:<span class=\"trace\">{$error['trace']}</span>";
+ }
+ break;
+ }
+ echo '</div>';
+ }
+
+ foreach ((array)$testRun['filters'] as $class => $data) {
+ echo $class::output('html', $data);
+ }
+
+ $tests = Group::all(array('transform' => true));
+ $exclude = '/\w+Test$|webroot|index$|^app\\\\config|^\w+\\\\views\/|\./';
+ $options = compact('exclude') + array('recursive' => true);
+ $classes = array_diff(Libraries::find('lithium', $options), $tests);
+ sort($classes);
+ ?>
+ <h3>Classes with no test case (<?php echo count($classes); ?>)</h3>
+ <ul class="classes">
+ <?php
+ foreach ($classes as $class) {
+ echo "<li>{$class}</li>";
+ }
+ ?>
+ </ul>
+
+ <h3>Included files (<?php echo count(get_included_files()); ?>)</h3>
+ <ul class="files">
+ <?php
+ $base = dirname(dirname($core));
+ $files = str_replace($base, '', get_included_files());
+ sort($files);
+
+ foreach ($files as $file) {
+ echo "<li>{$file}</li>";
+ }
+ ?>
+ </ul>
+ </div>
+ <div style="clear:both"></div>
+ </body>
+</html>
\ No newline at end of file
diff --git a/libraries/lithium/LICENSE.txt b/libraries/lithium/LICENSE.txt
new file mode 100644
index 0000000..fe30aad
--- /dev/null
+++ b/libraries/lithium/LICENSE.txt
@@ -0,0 +1,25 @@
+Copyright (c) 2009, Union of Rad, Inc. http://union-of-rad.org
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+ * Neither the name of Lithium, Union or Rad, nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
\ No newline at end of file
diff --git a/libraries/lithium/action/Controller.php b/libraries/lithium/action/Controller.php
new file mode 100644
index 0000000..cc27c32
--- /dev/null
+++ b/libraries/lithium/action/Controller.php
@@ -0,0 +1,215 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\action;
+
+use \Exception;
+
+/**
+ * The bulk of the controller code in 1.2 that we need to be concerned with can be broken down
+ * into these things:
+ *
+ * - Merging inherited configuration. We might possibly be able to handle this in a more
+ * generalized way and at a higher level. It may be a good use-case for a generic way
+ * to handle object configuration
+ *
+ * - Interacting with the view. This includes passing variables, determining formats and
+ * template/layout locations, and determining helpers.
+ *
+ * - Handling responses and flow control. This includes rendering, redirecting, and
+ * callbacks.
+ *
+ * - Action-oriented caching.
+ *
+ * - Pagination. I don't know how I feel about this. It kind of feels like it needs it's
+ * own object
+ */
+class Controller extends \lithium\core\Object {
+
+ public $request = null;
+
+ public $response = null;
+
+ protected $_render = array(
+ 'type' => 'html',
+ 'data' => array(),
+ 'auto' => true,
+ 'layout' => 'default',
+ 'template' => null,
+ 'hasRendered' => false
+ );
+
+ protected $_classes = array(
+ 'media' => '\lithium\http\Media',
+ 'router' => '\lithium\http\Router',
+ 'response' => '\lithium\action\Response'
+ );
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'request' => null, 'response' => array(),
+ 'render' => array(), 'classes' => array()
+ );
+ $config += $defaults;
+
+ if (!empty($config['request'])) {
+ $this->request = $config['request'];
+ }
+
+ foreach (array('render', 'classes') as $key) {
+ if (!empty($config[$key])) {
+ $this->{'_' . $key} = (array)$config[$key] + $this->{'_' . $key};
+ }
+ }
+ parent::__construct($config);
+ }
+
+ /**
+ * Called by the Dispatcher class to invoke an action.
+ *
+ * @param object $request The request object with URL and HTTP info for dispatching this action.
+ * @param array $dispatchParams The array of parameters that will be passed to the action.
+ * @param array $options The dispatch options for this action.
+ * @return object Returns the response object associated with this controller.
+ * @todo Implement proper exception catching/throwing
+ */
+ public function __invoke($request, $dispatchParams, $options = array()) {
+ $classes = $this->_classes;
+ $config = $this->_config;
+ $render =& $this->_render;
+
+ $filter = function($self, $params, $chain) use ($config, $classes, &$render) {
+ extract($params, EXTR_OVERWRITE);
+ $action = $dispatchParams['action'];
+ $args = isset($dispatchParams['args']) ? $dispatchParams['args'] : array();
+ $result = null;
+
+ if (substr($action, 0, 1) == '_' || method_exists(__CLASS__, $action)) {
+ throw new Exception('Private method!');
+ }
+
+ $response = $config['response'] + array('request' => $self->request);
+ $self->response = new $classes['response']($response);
+ $render['template'] = $render['template'] ?: $action;
+
+ try {
+ $result = $self->invokeMethod($action, $args);
+ } catch (Exception $e) {
+ // See todo, temporary alleviating obscure failure
+ throw $e;
+ }
+
+ if (!empty($result)) {
+ if (is_string($result)) {
+ $self->render(array('text' => $result));
+ } elseif (is_array($result)) {
+ $self->set($result);
+ }
+ }
+
+ if (!$render['hasRendered'] && $render['auto']) {
+ $self->render($action);
+ }
+ return $self->response;
+ };
+ return $this->_filter(__METHOD__, compact('dispatchParams', 'request', 'options'), $filter);
+ }
+
+ public function set($data = array()) {
+ $this->_render['data'] += (array)$data;
+ }
+
+ /**
+ * Uses results (typically coming from a controller action) to generate content and headers for
+ * a Response object.
+ *
+ * @param mixed $options A string template name (see the 'template' option below), or an array
+ * of options, as follows:
+ * - '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.
+ * - 'head': If true, only renders the headers of the response, not the body.
+ * Defaults to false.
+ * - 'data': An associative array of variables to be assigned to the template.
+ * These are merged on top of any variables set in `Controller::set()`.
+ * @return void
+ */
+ public function render($options = array()) {
+ if (is_string($options)) {
+ $options = array('template' => $options);
+ }
+ $defaults = array(
+ 'status' => 200, 'location' => false,
+ 'data' => array(), 'head' => false,
+ );
+ $options += $defaults;
+ $media = $this->_classes['media'];
+
+ if (!empty($options['data'])) {
+ $this->set($options['data']);
+ unset($options['data']);
+ }
+ $options = $options + $this->_render + array('request' => $this->request);
+ $type = key($options);
+ $types = array_flip($media::types());
+
+ if (isset($types[$type])) {
+ $options['type'] = $type;
+ $this->set(current($options));
+ unset($options[$type]);
+ }
+
+ $this->_render['hasRendered'] = true;
+ $this->response->type($options['type']);
+ $this->response->status($options['status']);
+ $this->response->headers('Location', $options['location']);
+
+ if ($options['head']) {
+ return;
+ }
+ $data = $this->_render['data'];
+ $data = (isset($data[0]) && count($data) == 1) ? $data[0] : $data;
+ $media::render($this->response, $data, $options);
+ }
+
+ /**
+ * Creates a redirect response.
+ *
+ * @param mixed $url
+ * @param array $options
+ * @return void
+ */
+ public function redirect($url, $options = array()) {
+ $router = $this->_classes['router'];
+ $defaults = array(
+ 'location' => $router::match($url, $this->request),
+ 'status' => 302,
+ 'head' => true,
+ 'exit' => true
+ );
+ $options += $defaults;
+
+ $this->_filter(__METHOD__, compact('options'), function($self, $params, $chain) {
+ $self->render($params['options']);
+ });
+
+ if ($options['exit']) {
+ $this->response->render();
+ $this->_stop();
+ }
+ return $this->response;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/action/Dispatcher.php b/libraries/lithium/action/Dispatcher.php
new file mode 100644
index 0000000..b539358
--- /dev/null
+++ b/libraries/lithium/action/Dispatcher.php
@@ -0,0 +1,172 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\core\Environment;
+
+class Dispatcher extends \lithium\core\StaticObject {
+
+ /**
+ * Fully-namespaced router class reference. Class must implement a `parse()` method,
+ * which must return an array with (at a minimum) 'controller' and 'action' keys.
+ *
+ * @see lithium\http\Router::parse()
+ * @var array
+ */
+ protected static $_classes = array(
+ 'request' => '\lithium\action\Request',
+ 'router' => '\lithium\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\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.:
+ * `array('action' => 'admin_{:action}')`. See `Dispatcher::config()` for examples
+ * on setting rules.
+ *
+ * @see lithium\action\Dispatcher::config()
+ * @see lithium\util\String::insert()
+ */
+ protected static $_rules = array();
+
+ /**
+ * Used to set configuration parameters for the Dispatcher.
+ *
+ * @param array $config
+ * @return array|void If no parameters are passed, returns an associative array with the
+ * current configuration, otherwise returns null.
+ */
+ public static function config($config = array()) {
+ if (empty($config)) {
+ return array('rules' => static::$_rules);
+ }
+
+ foreach ($config as $key => $val) {
+ if (isset(static::${'_' . $key})) {
+ static::${'_' . $key} = $val + static::${'_' . $key};
+ }
+ }
+ }
+
+ /**
+ * Dispatches a request based on a request object (an instance of `lithium\http\Request`). 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
+ * null, an instance will be created.
+ * @param array $options
+ * @return object
+ * @todo Add exception-handling/error page rendering
+ */
+ public static function run($request = null, $options = array()) {
+ $defaults = array('request' => array());
+ $options += $defaults;
+ $classes = static::$_classes;
+ $params = compact('request', 'options');
+ $m = __METHOD__;
+
+ return static::_filter($m, $params, function($self, $params, $chain) use ($classes) {
+ extract($params);
+
+ $router = $classes['router'];
+ $request = $request ?: new $classes['request']($options['request']);
+ $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));
+ });
+ }
+
+ protected static function _callable($request, $params, $options) {
+ $params = compact('request', 'params', 'options');
+ return static::_filter(__METHOD__, $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'));
+ }
+ throw new Exception("Controller {$class} not found");
+ });
+ }
+
+ protected static function _call($callable, $request, $params) {
+ $params = compact('callable', 'request', 'params');
+ return static::_filter(__METHOD__, $params, function($self, $params, $chain) {
+ $callable = $params['callable'];
+ if (is_callable($callable)) {
+ return $callable($params['request'], $params['params']);
+ }
+ throw new Exception('Result 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) {
+ $result = array();
+
+ if (!$params) {
+ return false;
+ }
+
+ foreach (static::$_rules as $rule => $value) {
+ foreach ($value as $k => $v) {
+ if (!empty($params[$rule])) {
+ $result[$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 $result + array_diff_key($params, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/action/Request.php b/libraries/lithium/action/Request.php
new file mode 100644
index 0000000..f79e052
--- /dev/null
+++ b/libraries/lithium/action/Request.php
@@ -0,0 +1,360 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\action;
+
+use \lithium\util\Validator;
+
+class Request extends \lithium\core\Object {
+
+ public $url = null;
+
+ public $params = array();
+
+ public $data = array();
+
+ public $query = array();
+
+ /**
+ * Holds the environment variables for the request. Retrieved with env().
+ *
+ * @var array
+ * @see lithium\http\Request::env()
+ */
+ protected $_env = array();
+
+ protected $_type = 'html';
+
+ protected $_base = null;
+
+ protected $_classes = array('media' => '\lithium\http\Media');
+
+ protected $_detectors = array(
+ 'mobile' => array('HTTP_USER_AGENT', null),
+ 'ajax' => array('HTTP_X_REQUESTED_WITH', 'XMLHttpRequest'),
+ 'flash' => array('HTTP_USER_AGENT', 'Shockwave Flash'),
+ 'ssl' => 'HTTPS'
+ );
+
+ /**
+ * 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.
+ *
+ * @var array
+ */
+ protected $_acceptTypes = array();
+
+ protected $_autoConfig = array('classes' => 'merge', 'detectors' => 'merge', 'base', 'type');
+
+ /**
+ * Pulls request data from superglobals.
+ *
+ * @return void
+ * @todo Replace $_FILES loops with Felix's code (or Marc's?)
+ * @todo Consider disabling magic quotes stripping, or only having it explicitly enabled, since
+ * it's deprecated now.
+ */
+ protected function _init() {
+ parent::_init();
+ $this->_base = $this->_base ?: $this->_base();
+
+ $m = '/(iPhone|MIDP|AvantGo|BlackBerry|J2ME|Opera Mini|DoCoMo|NetFront|Nokia|PalmOS|';
+ $m .= 'PalmSource|portalmmm|Plucker|ReqwirelessWeb|SonyEricsson|Symbian|UP\.Browser|';
+ $m .= 'Windows CE|Xiino)/i';
+ $this->_detectors['mobile'][1] ?: $m;
+
+ $this->url = isset($_GET['url']) ? rtrim($_GET['url'], '/') : '';
+ $this->url = $this->url ?: '/';
+ $this->_env = (array)$_SERVER + (array)$_ENV;
+
+ $envs = array('isapi' => 'IIS', 'cgi' => 'cgi');
+ $env = php_sapi_name();
+ $this->_env['PLATFORM'] = array_key_exists($env, $envs) ? $envs[$env] : null;
+
+ if (!empty($_POST)) {
+ $this->data = $_POST;
+
+ if (!empty($this->_env['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
+ $this->data['_method'] = $this->_env['HTTP_X_HTTP_METHOD_OVERRIDE'];
+ }
+
+ if (isset($this->data['_method'])) {
+ if (isset($_SERVER) && !empty($_SERVER)) {
+ $_SERVER['REQUEST_METHOD'] = $params['data']['_method'];
+ } else {
+ $_ENV['REQUEST_METHOD'] = $params['data']['_method'];
+ }
+ unset($this->params['form']['_method']);
+ }
+ }
+
+ if (!empty($_FILES)) {
+ foreach ($_FILES as $name => $data) {
+ if ($name != 'data') {
+ $params['form'][$name] = $data;
+ }
+ }
+
+ // Replace this:
+ if (isset($_FILES['data'])) {
+ foreach ($_FILES['data'] as $key => $data) {
+ foreach ($data as $model => $fields) {
+ foreach ($fields as $field => $value) {
+ if (is_array($value)) {
+ $params['data'][$model][$field][key($value)][$key] = current($value);
+ } else {
+ $params['data'][$model][$field][$key] = $value;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Queries PHP's environment settings, and provides an abstraction for standardizing expected
+ * environment values across varying platforms, as well as specify custom environment flags.
+ *
+ * @param string $key
+ * @return void
+ * @todo Refactor to lazy-load environment settings
+ */
+ public function env($key) {
+ if ($key == 'base') {
+ return $this->_base;
+ }
+
+ if ($key == 'HTTPS') {
+ if (array_key_exists($this->_env['HTTPS'])) {
+ return (isset($this->_env['HTTPS']) && $this->_env['HTTPS'] == 'on');
+ }
+ return (strpos($this->_env['SCRIPT_URI'], 'https://') === 0);
+ }
+
+ if ($key == 'SCRIPT_NAME') {
+ if ($this->_env['PLATFORM'] == 'CGI' || isset($this->_env['SCRIPT_URL'])) {
+ $key = 'SCRIPT_URL';
+ }
+ }
+ $val = null;
+
+ if (!array_key_exists($key, $this->_env)) {
+ $val = getenv($key);
+ } else {
+ $val = $this->_env[$key];
+ }
+
+ if ($key == 'REMOTE_ADDR' && $val == $this->env('SERVER_ADDR')) {
+ if (($addr = $this->env('HTTP_PC_REMOTE_ADDR')) != null) {
+ $val = $addr;
+ }
+ }
+
+ if ($val !== null && $val !== false) {
+ return $val;
+ }
+
+ switch ($key) {
+ case 'SCRIPT_FILENAME':
+ if ($this->_env['PLATFORM'] == 'IIS') {
+ return str_replace('\\\\', '\\', $this->env('PATH_TRANSLATED'));
+ }
+ break;
+ case 'DOCUMENT_ROOT':
+ $fileName = $this->env('SCRIPT_FILENAME');
+ $offset = (!strpos($this->env('SCRIPT_NAME'), '.php')) ? 4 : 0;
+ $offset = strlen($fileName) - (strlen($this->env('SCRIPT_NAME')) + $offset);
+ return substr($fileName, 0, $offset);
+ break;
+ case 'PHP_SELF':
+ return str_replace($this->env('DOCUMENT_ROOT'), '', $this->env('SCRIPT_FILENAME'));
+ break;
+ case 'CGI':
+ case 'CGI_MODE':
+ return ($this->_env['PLATFORM'] == 'CGI');
+ break;
+ case 'HTTP_BASE':
+ return preg_replace ('/^([^.])*/i', null, $this->_env['HTTP_HOST']);
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * undocumented function
+ *
+ * @param string $key
+ * @return void
+ */
+ public function get($key) {
+ list($var, $key) = explode(':', $key);
+
+ switch (true) {
+ case in_array($var, array('params', 'data', 'query')):
+ return isset($this->{$key[0]}[$key[1]]) ? $this->{$key[0]}[$key[1]] : null;
+ break;
+ case ($var == 'env'):
+ return $this->env($key);
+ break;
+ }
+ return null;
+ }
+
+ /**
+ * Detects properties of the request and returns a boolean response
+ *
+ * @return boolean
+ * @see lithium\http\Request::detect()
+ * @todo Remove $content and refer to Media class instead
+ */
+ public function is($flag) {
+ $flag = strtolower($flag);
+ $content = array('xml', 'rss', 'atom');
+
+ if (array_key_exists($flag, $this->_detectors)) {
+ $detector = $this->_detectors[$flag];
+
+ if (is_array($detector)) {
+ if (is_string($detector[1]) && Validator::isRegex($detector[1])) {
+ return (bool)preg_match($detector[1], $this->env($detector[0]));
+ }
+ return ($this->env($detector[0]) == $detectors[1]);
+ } elseif (is_object($detector)) {
+ return $detector($this);
+ }
+ return (bool)$this->env($detector);
+ }
+ return false;
+ }
+
+ /**
+ * Returns the content type of the response.
+ *
+ * @return string A simple content type name, i.e. `'html'`, `'xml'`, `'json'`, etc., depending
+ * on the content type of the request.
+ */
+ public function type() {
+ return $this->_type;
+ }
+
+ /**
+ * Creates a 'detector' used with Request::is(). A detector is a boolean check that is created
+ * to determine something about a request.
+ *
+ * @return void
+ * @see lithium\http\Request::is()
+ */
+ public function detect($flag, $detector = null) {
+ if (is_array($flag)) {
+ $this->_detectors = $flag + $this->_detectors;
+ } else {
+ $this->_detectors[$flag] = $detector;
+ }
+ }
+
+ /**
+ * Gets the referring URL of this request
+ *
+ * @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
+ * @access public
+ * @todo Rewrite me to remove constant dependencies
+ */
+ function referer($default = null, $local = false) {
+ $ref = $this->env('HTTP_REFERER');
+ if (!empty($ref) && defined('FULL_BASE_URL')) {
+ $base = FULL_BASE_URL . $this->webroot;
+ if (strpos($ref, $base) === 0) {
+ $return = substr($ref, strlen($base));
+ if ($return[0] != '/') {
+ $return = '/'.$return;
+ }
+ return $return;
+ } elseif (!$local) {
+ return $ref;
+ }
+ }
+ return ($default != null) ? $default : '/';
+ }
+
+ public function normalizeFiles($original) {
+ $r = array();
+
+ $files = array();
+
+ foreach ($original as $file) {
+ if (!is_array($file['name'])) {
+ $files[] = $file;
+ continue;
+ }
+ $nested = array();
+
+ foreach ($file as $key => $items) {
+ while (is_array(current($items))) {
+ $items = $items[key($items)];
+ }
+ $items = array_values($items);
+
+ foreach ($items as $i => $item) {
+ $nested[$i][$key] = $item;
+ }
+ }
+ $files = array_merge($files, $nested);
+ }
+ return $files;
+
+ foreach ($files as $field => $values) {
+ if (!is_array($values['name'])) {
+ $r[$field] = $values;
+ continue;
+ }
+ foreach ($values as $key => $fields) {
+ while ($fields) {
+ foreach ($fields as $tmpField => $val) {
+ unset($fields[$tmpField]);
+ if (!is_array($val)) {
+ $tmpField = preg_replace('/(^[^\[]+)(.*)/', '[\\1]\\2', $tmpField);
+ $r[$field . $tmpField][$key] = $val;
+ continue;
+ }
+ foreach ($val as $subField => $subVal) {
+ $fields[$tmpField . '[' . $subField . ']'] = $subVal;
+ }
+ }
+ }
+ }
+ }
+
+ foreach ($r as $field => $values) {
+ $r[$field] = compact('field') + $values;
+ }
+ return array_values($r);
+ }
+
+ /**
+ * @todo Replace string directory names with configuration
+ * @return void
+ */
+ protected function _base() {
+ $base = dirname($this->env('PHP_SELF'));
+
+ while (in_array(basename($base), array('app', 'webroot'))) {
+ $base = dirname($base);
+ }
+ return $base;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/action/Response.php b/libraries/lithium/action/Response.php
new file mode 100644
index 0000000..5a9fada
--- /dev/null
+++ b/libraries/lithium/action/Response.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\action;
+
+use \Exception;
+
+class Response extends \lithium\http\Response {
+
+ protected $_config = array();
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'buffer' => 8192, 'request' => null, 'defaultType' => 'html'
+ );
+ if (!empty($config['request']) && is_object($config['request'])) {
+ $this->type = $config['request']->type();
+ }
+ $this->_config = (array)$config + $defaults;
+ parent::__construct($this->_config);
+ }
+
+ /**
+ * Content Type
+ *
+ * @return string
+ */
+ public function type($type = null) {
+ if (!empty($type)) {
+ return $this->type = $type;
+ }
+ return $this->type ?: $this->_config['defaultType'];
+ }
+
+ /**
+ * Disables HTTP caching for web clients and proxies.
+ *
+ * @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'
+ ));
+ }
+
+ /**
+ * Render a response by writing headers and output. Output is echoed in chunks because of an
+ * issue where `echo` time increases exponentially on long message bodies.
+ *
+ * @return void
+ */
+ public function render() {
+ $code = null;
+ if (isset($this->headers['location'])) {
+ $code = 302;
+ }
+ $status = $this->status($code);
+ if (!$status) {
+ throw new Exception('Invalid status code');
+ }
+
+ $this->_writeHeader($status);
+
+ foreach ($this->headers as $name => $value) {
+ $key = strtolower($name);
+ if ($key == 'location') {
+ $this->_writeHeader("Location: {$value}", $this->status['code']);
+ } elseif ($key == 'download') {
+ $tmp = 'Content-Disposition: attachment;'
+ . ' filename="' . $value . '"';
+ $this->_writeHeader($tmp);
+ } elseif (is_array($value)) {
+ $this->_writeHeader(
+ array_map(function($v) use ($name) { return "{$name}: {$v}"; }, $value)
+ );
+ } elseif (!is_numeric($name)) {
+ $this->_writeHeader("{$name}: {$value}");
+ }
+ }
+ $chunked = str_split(join("\r\n", (array)$this->body), $this->_config['buffer']);
+
+ foreach ($chunked as $chunk) {
+ echo $chunk;
+ }
+ }
+
+ /**
+ * Casts the Response object to a string. This doesn't actually return a string, but does
+ * a direct render and returns null.
+ *
+ * @return void
+ */
+ public function __toString() {
+ $this->render();
+ return '';
+ }
+
+ /**
+ * Writes raw headers to output.
+ *
+ * @param mixed $header Either a raw header string, or an array of header strings. Use an array
+ * if a single header must be written multiple times with different values.
+ * Otherwise, subsequent values with non-unique header names will overwrite
+ * previous values.
+ * @param int $code Optional. If present, forces a specific HTTP response code. Used primarily
+ * in conjunction with the 'Location' header.
+ * @return void
+ */
+ protected function _writeHeader($header, $code = null) {
+ if (is_array($header)) {
+ array_map(function($h) { header($h, false); }, $header);
+ return;
+ }
+ $code ? header($header, true) : header($header, true, $code);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/Command.php b/libraries/lithium/console/Command.php
new file mode 100644
index 0000000..db16669
--- /dev/null
+++ b/libraries/lithium/console/Command.php
@@ -0,0 +1,326 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console;
+
+use \Exception;
+use \ReflectionClass;
+use \lithium\util\Inflector;
+use \lithium\util\reflection\Docblock;
+use \lithium\core\Libraries;
+
+/**
+ * The base class to inherit when writing Console scripts in Lithium.
+ *
+ */
+class Command extends \lithium\core\Object {
+
+ /**
+ * A Request object
+ *
+ * @var lithium\console\Request
+ */
+ public $request;
+
+ /**
+ * A Response object
+ *
+ * @var lithium\console\Response
+ */
+ public $response;
+
+ /**
+ * classes used by Command
+ *
+ * @var string
+ */
+ protected $_classes = array(
+ 'response' => '\lithium\console\Response'
+ );
+
+ /**
+ * Constrcutor
+ *
+ * @param array config
+ * @return void
+ */
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'request' => null, 'response' => array(), 'classes' => array()
+ );
+ $config += $defaults;
+
+ if (!empty($config['request'])) {
+ $this->request = $config['request'];
+ }
+
+ if (!empty($config['classes'])) {
+ $this->{'_' . $key} = (array)$config[$key] + $this->{'_' . $key};
+ }
+
+ parent::__construct($config);
+ }
+
+ public function _init() {
+ $config = (array)$this->_config['response'] + array('request' => $this->request);
+ $this->response = new $this->_classes['response']($config);
+ }
+
+ /**
+ * initialize callback
+ *
+ * @return void
+ */
+ public function initialize() {
+
+ }
+
+ /**
+ * base method, shows list of available commands
+ * override in subclasses
+ *
+ * @return void
+ */
+ public function run() {
+ $this->header('Available Commands');
+
+ foreach (Libraries::locate('commands') as $command) {
+ $command = explode('\\', $command);
+ $this->out(' - ' . Inflector::underscore(array_pop($command)));
+ }
+ }
+
+ /**
+ * Called by the Dispatcher class to invoke an action
+ *
+ * @param string $action
+ * @param array $params
+ * @return object Returns the response object associated with this controller
+ * @todo Implement proper exception catching/throwing
+ * @todo Implement filters
+ */
+ public function __invoke($action, $passed = array(), $options = array()) {
+ $result = null;
+
+ try {
+ foreach ((array)$this->request->params['named'] as $key => $param) {
+ $this->{$key} = $param;
+ }
+ $this->initialize();
+ $result = $this->invokeMethod($action, $passed);
+ } catch (Exception $e) {
+ // See todo
+ }
+ return $result;
+ }
+
+ /**
+ * Writes string to output stream
+ *
+ * @param string $str
+ * @param integer $newlines
+ * @return boolean
+ */
+ public function out($str = null, $newlines = 1) {
+ if (is_array($str)) {
+ foreach ($str as $string) {
+ $this->out($string, $newlines);
+ }
+ return;
+ }
+ if ($newlines) {
+ $str = $str . str_pad("\n", $newlines, "\n");
+ }
+ return $this->response->output($str);
+ }
+
+ /**
+ * Writes string to error stream
+ *
+ * @param string $str
+ * @param integer $newlines
+ * @return boolean
+ */
+ public function err($str = null, $newlines = 1) {
+ if (is_array($str)) {
+ foreach ($str as $string) {
+ $this->err($string, $newlines);
+ }
+ return;
+ }
+ if ($newlines) {
+ $str = $str . str_pad("\n", $newlines, "\n");
+ }
+ return $this->response->error($str);
+ }
+
+ /**
+ * Handles input. Will continue to loop until
+ * options['quit'] or result is part of options['options']
+ *
+ * @param string $prompt
+ * @param string $options
+ * @param string $default
+ * @return string
+ */
+ public function in($prompt = null, $options = array()) {
+ $defaults = array('choices' => null, 'default' => null, 'quit' => 'q');
+ $options += $defaults;
+
+ $choices = null;
+ if (is_array($options['choices'])) {
+ $choices = '(' . implode('/', $options['choices']) . ')';
+ }
+
+ if ($options['default'] == null) {
+ $this->out("{$prompt} {$choices} \n > ", false);
+ } else {
+ $this->out("{$prompt} {$choices} \n [{$options['default']}] > ", false);
+ }
+
+ $result = null;
+ do {
+ $result = trim($this->request->input());
+ } while (
+ $result == null && !empty($options['quit']) && $result != $options['quit']
+ && !empty($options['options']) && array_search($result, $options['options'])
+ );
+
+ if ($options['default'] != null && empty($result)) {
+ return $options['default'] ;
+ }
+ return $result;
+ }
+
+ /**
+ * Add text with horizontal line before and after stream
+ *
+ * @param integer $length
+ * @param integer $newlines
+ * @return string
+ */
+ public function header($text, $line = 80) {
+ $this->hr($line);
+ $this->out($text);
+ $this->hr($line);
+ }
+
+ /**
+ * Writes rows of columns
+ *
+ * @param array $rows
+ * @param string $separator (default "\t")
+ * @return string
+ */
+ public function columns($rows, $separator = "\t") {
+ $lengths = array_reduce($rows, function($columns, $row) {
+ foreach ((array)$row as $key => $val) {
+ if (!isset($columns[$key]) || strlen($val) > $columns[$key]) {
+ $columns[$key] = strlen($val);
+ }
+ }
+ return $columns;
+ });
+ $rows = array_reduce($rows, function($rows, $row) use ($lengths, $separator) {
+ $text = '';
+ foreach ((array)$row as $key => $val) {
+ $text = $text . str_pad($val, $lengths[$key]) . $separator;
+ }
+ $rows[] = $text;
+ return $rows;
+ });
+ $this->out($rows);
+ }
+
+ /**
+ * Add new lines to output stream
+ *
+ * @param integer $number
+ * @return string
+ */
+ public function nl($number = 1) {
+ return $this->out(null, $number);
+ }
+
+ /**
+ * Add horizontal line to output stream
+ *
+ * @param integer $length
+ * @param integer $newlines
+ * @return string
+ */
+ public function hr($length = 80, $newlines = 1) {
+ return $this->out(str_repeat('-', $length), $newlines);
+ }
+
+ /**
+ * Stop execution with exit
+ *
+ * @param integer $status
+ * @param boolean $message
+ * @return string
+ */
+ public function stop($status = 0, $message = null) {
+ if (!is_null($message)) {
+ if ($status == 0) {
+ $this->out($message);
+ } else {
+ $this->err($message);
+ }
+ }
+ exit($status);
+ }
+
+ /**
+ * Will show basic help for the command
+ *
+ * @return void
+ */
+ public function help() {
+ $parent = new ReflectionClass("\lithium\console\Command");
+ $class = new ReflectionClass(get_class($this));
+
+ $params = array();
+ $template = $class->newInstance();
+ $properties = array_diff($class->getProperties(), $parent->getProperties());
+ $propertyFilter = function($prop) {
+ return $prop->isPublic() && !preg_match('/^[A-Z]/', $prop->getName());
+ };
+
+ foreach ((array)array_filter($properties, $propertyFilter) as $property) {
+ $hint = null;
+ $val = $property->getValue($template);
+
+ if (!is_bool($val)) {
+ $hint = '=val';
+ $comment = Docblock::comment($property->getDocComment());
+ if (isset($comment['tags']['var'])) {
+ $hint = '=' . strtoupper($comment['tags']['var']);
+ }
+ }
+ $name = str_replace('_', '-', Inflector::underscore($property->getName()));
+ $params[] = sprintf('[--%s%s]', $name, $hint);
+ }
+
+ // Show parameters as well
+ $className = explode("\\", $class->getName());
+ $command = array_pop($className);
+ $this->out(sprintf(
+ 'usage: lithium %s %s', $command, join(' ', $params)
+ ), 2);
+
+ $comment = Docblock::comment($class->getDocComment());
+ $this->out($comment['description']);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/Dispatcher.php b/libraries/lithium/console/Dispatcher.php
new file mode 100644
index 0000000..4fc3b05
--- /dev/null
+++ b/libraries/lithium/console/Dispatcher.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Dispatcher extends \lithium\core\Object {
+
+ /**
+ * Fully-namespaced router class reference. Class must implement a `parse()` method,
+ * which must return an array with (at a minimum) 'command' and 'action' keys.
+ *
+ * @see lithium\console\Router::parse()
+ * @var array
+ */
+ protected static $_classes = array(
+ 'request' => '\lithium\console\Request',
+ 'router' => '\lithium\console\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\console\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.:
+ * `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}')
+ );
+
+ /**
+ * Used to set configuration parameters for the Dispatcher.
+ *
+ * @param array $config
+ * @return array|void If no parameters are passed, returns an associative array with the
+ * current configuration, otherwise returns null.
+ */
+ public static function config($config = array()) {
+ if (empty($config)) {
+ return array('rules' => static::$_rules);
+ }
+
+ foreach ($config as $key => $val) {
+ if (isset(static::${'_' . $key})) {
+ static::${'_' . $key} = $val + static::${'_' . $key};
+ }
+ }
+ }
+
+ /**
+ * Dispatches a request based on a request object (an instance of `lithium\console\Request`). 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
+ * null, an instance will be created.
+ * @param array $options
+ * @return object
+ * @todo Add exception-handling/error page rendering
+ */
+ public static function run($request = null, $options = array()) {
+ $defaults = array();
+ $options += $defaults;
+
+ if (empty($request)) {
+ $request = new static::$_classes['request']($options);
+ }
+ $router = static::$_classes['router'];
+ $request->params = static::_applyRules($router::parse($request));
+ $class = $request->params['command'] ?: '\lithium\console\Command';
+
+ if ($class[0] !== '\\') {
+ $class = Libraries::locate('commands', Inflector::camelize($class));
+ }
+
+ $isRun = (
+ $request->params['action'] != 'run'
+ && !method_exists($class, $request->params['action'])
+ );
+
+ if ($isRun) {
+ array_unshift($request->params['passed'], $request->params['action']);
+ $request->params['action'] = 'run';
+ }
+
+ if (!class_exists($class)) {
+ throw new UnexpectedValueException("Command $class not found");
+ }
+
+ $command = new $class(compact('request'));
+ return $command($request->params['action'], $request->params['passed']);
+ }
+
+ /**
+ * 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
new file mode 100644
index 0000000..8eecdb6
--- /dev/null
+++ b/libraries/lithium/console/Request.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\console;
+
+/**
+ * Holds current request from console
+ *
+ * @package lithium.console
+ *
+ **/
+class Request extends \lithium\core\Object {
+
+ /**
+ * Enviroment variables
+ * - pwd path to where script is running
+ * - working current directory
+ *
+ * @var array
+ **/
+ public $env = array();
+
+ /**
+ * Arguments from the console
+ *
+ * @var array
+ **/
+ public $args = array();
+
+ /**
+ * Params from router
+ *
+ * @var array
+ **/
+ public $params = array(
+ 'command' => null, 'action' => 'run',
+ 'passed' => array(), 'named' => array()
+ );
+
+ /**
+ * Input stream, STDIN
+ *
+ * @var stream
+ **/
+ public $input = null;
+
+ /**
+ * Construct Request object
+ *
+ * @param array $config
+ * - init boolean runs _init method
+ * [default] false
+ * - args array
+ * [default] empty
+ * - env array
+ * [default] working => LITHIUM_APP_PATH
+ * - input stream
+ *
+ * @access public
+ * @return void
+ *
+ **/
+ public function __construct($config = array()) {
+ $config += array(
+ 'init' => false,
+ 'argv' => array(),
+ 'args' => array(),
+ 'env' => array(),
+ 'input' => null,
+ );
+
+ if (!empty($_SERVER['argv'])) {
+ $this->args += $_SERVER['argv'];
+ }
+
+ $this->args += $config['argv'];
+
+ $this->env['working'] = LITHIUM_APP_PATH;
+
+ if (!empty($_SERVER['PWD'])) {
+ $this->env['working'] = $_SERVER['PWD'];
+ }
+ $working = array_search('-working', $this->args);
+
+ if ($working && !empty($this->args[$working])) {
+ $this->env['working'] = $this->args[$working + 1];
+ $this->args = array_slice($this->args, 3);
+ }
+ $this->args = $config['args'] + $this->args;
+ $this->env = $config['env'] + $this->env;
+ $this->input = $config['input'];
+
+ if (!is_resource($this->input)) {
+ $this->input = fopen('php://stdin', 'r');
+ }
+ }
+
+ /**
+ * Return input
+ *
+ * @return void
+ *
+ **/
+ public function input() {
+ return fgets($this->input);
+ }
+
+ /**
+ * Destructor to close streams
+ *
+ * @return void
+ *
+ **/
+ public function __destruct() {
+ fclose($this->input);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/Response.php b/libraries/lithium/console/Response.php
new file mode 100644
index 0000000..6d7c10a
--- /dev/null
+++ b/libraries/lithium/console/Response.php
@@ -0,0 +1,107 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\console;
+
+/**
+ * Holds current request from console
+ *
+ * @package lithium.console
+ *
+ **/
+class Response extends \lithium\core\Object {
+
+ /**
+ * A Request object
+ *
+ * @var lithium\console\Request
+ **/
+ public $request;
+
+ /**
+ * Output stream, STDOUT
+ *
+ * @var stream
+ **/
+ public $output = null;
+
+ /**
+ * Error stream, STDERR
+ *
+ * @var stream
+ **/
+ public $error = null;
+
+ /**
+ * Construct Request object
+ *
+ * @param array $config
+ * - request object lithium\console\Request
+ * - output stream
+ * _ error stream
+ * @access public
+ * @return void
+ *
+ **/
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'request' => null, 'output' => null, 'error' => null
+ );
+ $config += $defaults;
+
+ if (!empty($config['request'])) {
+ $this->request = $config['request'];
+ }
+
+ $this->output = $config['output'];
+ if (!is_resource($this->output)) {
+ $this->output = fopen('php://stdout', 'r');;
+ }
+
+ $this->error = $config['error'];
+ if (!is_resource($this->error)) {
+ $this->error = fopen('php://stderr', 'r');;
+ }
+ parent::__construct($config);
+ }
+
+ /**
+ * Writes string to output stream
+ *
+ * @param string $string
+ * @return mixed
+ */
+ public function output($string) {
+ return fwrite($this->output, $string);
+ }
+
+ /**
+ * Writes string to error stream
+ *
+ * @param string $str
+ * @return mixed
+ */
+ public function error($string) {
+ return fwrite($this->error, $string);
+ }
+
+ /**
+ * Destructor to close streams
+ *
+ * @return void
+ *
+ **/
+ public function __destruct() {
+ fclose($this->output);
+ fclose($this->error);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/Router.php b/libraries/lithium/console/Router.php
new file mode 100644
index 0000000..2c00997
--- /dev/null
+++ b/libraries/lithium/console/Router.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\console;
+
+/**
+ * Router parses incoming request
+ *
+ * @package lithium.console
+ *
+ **/
+class Router extends \lithium\core\Object {
+
+ /**
+ * Parse incoming request from console
+ *
+ * @param object $request \lithium\console\Request
+ * @return array command, passed, named
+ *
+ **/
+ public static function parse($request = null) {
+ $params = array(
+ 'command' => null, 'action' => 'run',
+ 'passed' => array(), 'named' => array()
+ );
+
+ if (!empty($request->params)) {
+ $params = $request->params + $params;
+ }
+
+ if (!empty($request->args)) {
+ $args = $request->args;
+ if (!isset($request->params['command'])) {
+ $params['command'] = array_shift($args);
+ }
+
+ while ($arg = array_shift($args)) {
+ if (preg_match('/^-(?P<key>[a-zA-Z0-9]+)$/', $arg, $match)) {
+ $arg = array_shift($args);
+ $params['named'][$match['key']] = $arg;
+ continue;
+ }
+
+ if (preg_match('/^--(?P<key>[a-z0-9-]+)(?:=(?P<val>.+))?$/', $arg, $match)) {
+ array_unshift($args, $arg);
+ $params['named'][$match['key']] = (!isset($match['val'])) ? true : $match['val'];
+ break;
+ }
+ $params['passed'][] = $arg;
+ }
+ }
+
+ if (!empty($params['passed'][0])) {
+ $params['action'] = $params['passed'][0];
+ unset($params['passed'][0]);
+ $params['passed'] = array_values($params['passed']);
+ }
+ return $params;
+ }
+}
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/Docs.php b/libraries/lithium/console/commands/Docs.php
new file mode 100644
index 0000000..3ff6959
--- /dev/null
+++ b/libraries/lithium/console/commands/Docs.php
@@ -0,0 +1,31 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands;
+
+use \lithium\console\commands\docs\Generator;
+/**
+ * Adds headers and docblocks to classes and methods
+ *
+ **/
+class Docs extends \lithium\console\Command {
+
+ public function run() {
+
+ }
+
+ public function generator() {
+ $generator = new Generator(array('request' => $this->request));
+ return $generator->run();
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/G11n.php b/libraries/lithium/console/commands/G11n.php
new file mode 100644
index 0000000..e8dc693
--- /dev/null
+++ b/libraries/lithium/console/commands/G11n.php
@@ -0,0 +1,104 @@
+<?php
+
+namespace lithium\console\commands;
+
+use \Exception;
+use \DateTime;
+use \lithium\g11n\Catalog;
+
+class G11n extends \lithium\console\Command {
+
+ public function main() {
+ Console::out('G11n Script');
+ Console::hr('=', 2);
+
+ $sourcePath = LITHIUM_APP_PATH;
+ $destinationPath = LITHIUM_APP_PATH . '/locales/po/';
+
+ Console::out('Extracting messages from source code.');
+ Console::hr('-', 2);
+ $timeStart = microtime(true);
+
+ $data = $this->_extract($sourcePath);
+
+ Console::nl();
+ Console::out('Yielded {:countItems} items taking {:duration} seconds.', array(
+ 'countItems' => count($data),
+ 'duration' => round(microtime(true) - $timeStart, 4)
+ ));
+
+ Console::nl();
+ Console::out('Additional data.');
+ Console::hr('-', 2);
+
+ $meta = $this->_meta();
+
+ Console::nl();
+ Console::out('Messages template.');
+ Console::hr('-', 2);
+
+ $message = 'Would you like to save the template now? ';
+ $message .= '(An existing template will be overwritten)';
+ if (Console::in($message, 'n', 'y/n') != 'y') {
+ Console::stop(1, 'Aborting upon user request.');
+ }
+ Console::nl();
+
+ $this->_writeTemplate($data, $meta);
+
+ Console::nl();
+ Console::out('Done.');
+ }
+
+ /**
+ * Extracts translatable strings from multiple files.
+ *
+ * @param array $files Absolute paths to files
+ * @return array
+ */
+ function _extract($path) {
+ Catalog::config(array(
+ 'extract' => array('adapter' => 'Code', 'path' => $path)
+ ));
+ return Catalog::read('message.template', 'root', array('name' => 'extract'));
+ }
+
+ /**
+ * Prompts for addtional data.
+ *
+ * @return array
+ */
+ protected function _meta() {
+ $now = new DateTime();
+ return array(
+ 'package' => Console::in('Package name:', null, 'app'),
+ 'packageVersion' => Console::in('Package version:'),
+ 'copyrightYear' => Console::in('Copyright year:', null, $now->format('Y')),
+ 'copyright' => Console::in('Copyright holder:'),
+ 'copyrightEmail' => Console::in('Copyright email address:'),
+ 'templateCreationDate' => $now->format('Y-m-d H:iO'),
+ );
+ }
+
+ /**
+ * Prompts for data source and writes template.
+ *
+ * @param array $data Data to save
+ * @param array $meta Additional data to save
+ * @return void
+ * @todo readd meta data
+ */
+ protected function _writeTemplate($data, $meta) {
+ $configs = array_keys(Catalog::config());
+
+ foreach ($configs as $key => $config) {
+ Console::out($key);
+ }
+ $key = Console::in('Please choose a config:');
+ $name = $configs[$key];
+
+ Catalog::write('message.template', 'root', compact('name'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/Generate.php b/libraries/lithium/console/commands/Generate.php
new file mode 100644
index 0000000..d611155
--- /dev/null
+++ b/libraries/lithium/console/commands/Generate.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands;
+
+class Generate {
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/Test.php b/libraries/lithium/console/commands/Test.php
new file mode 100644
index 0000000..e35802c
--- /dev/null
+++ b/libraries/lithium/console/commands/Test.php
@@ -0,0 +1,173 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands;
+
+use \lithium\core\Libraries;
+use \lithium\test\Group;
+use \lithium\test\Dispatcher;
+use \lithium\util\reflection\Inspector;
+
+/**
+ * Runs a given set unit tests and outputs the results.
+ */
+class Test extends \lithium\console\Command {
+
+ /**
+ * path to test case in dot notation
+ * example: lithium test -case console.CommandTest
+ *
+ * @var string
+ */
+ public $case = null;
+
+ /**
+ * path to test group in dot notation
+ * example: lithium test -group console
+ *
+ * @var string
+ */
+ public $group = null;
+
+ /**
+ * filters
+ *
+ * @var string
+ */
+ public $filters = array();
+
+ /**
+ * Runs tests. Will provide a list of available tests if none are give
+ * Test cases should be given in dot notation.
+ * case example: lithium test -case lithium.tests.cases.core.ObjectTest
+ * group example: lithium test -group lithium.tests.cases.core
+ *
+ * @return void
+ */
+ public function run() {
+ if ($this->_getTests() != true) {
+ return 0;
+ }
+ $startBenchmark = microtime(true);
+
+ error_reporting(E_ALL | E_STRICT | E_DEPRECATED);
+
+ if (!empty($this->case)) {
+ $this->case = 'lithium\tests\cases\\' . str_replace('.', '\\',
+ str_replace('lithium.tests.cases.', '', $this->case)
+ );
+ }
+ if (!empty($this->group)) {
+ $this->group = '\\' . str_replace('.', '\\',
+ str_replace('lithium.tests.cases.', '', $this->group)
+ );
+ }
+ $testRun = Dispatcher::run(null, array(
+ 'case' => $this->case, 'group' => $this->group,
+ 'filters' => $this->filters
+ ));
+
+ $stats = Dispatcher::process($testRun['results']);
+
+ $this->header('Included Files');
+ $base = dirname(dirname(dirname(dirname(__DIR__))));
+ $files = str_replace($base, '', get_included_files());
+ sort($files);
+ $this->out($files);
+
+ $passes = count($stats['passes']);
+ $fails = count($stats['fails']);
+ $exceptions = count($stats['exceptions']);
+
+ $this->header($testRun['title']);
+ $this->out("{$passes} / {$stats['asserts']} passes");
+ $this->out(($stats['fails'] === 1) ? "{$fails} fail" : "{$fails} fails");
+ $this->out("{$exceptions} exceptions");
+
+ foreach ((array)$stats['fails'] as $fail) {
+ $this->out("Assertion '{$fail['assertion']}' failed in");
+ $this->out("{$fail['class']}::{$fail['method']}() on line {$fail['line']}");
+ $this->out($fail['message']);
+ }
+
+ foreach ((array)$testRun['filters'] as $class => $data) {
+ $this->out($class::output('text', $data));
+ }
+
+ $this->header('Benchmarking');
+ $this->out("Time: " . number_format(microtime(true) - $startBenchmark, 4) . 's');
+ $this->out("Peak Memory: " . number_format((memory_get_peak_usage() / 1024), 3) . 'k');
+ $this->out("Current Memory: " . number_format((memory_get_usage() / 1024), 3) . 'k');
+
+ $again = $this->in("Would you like to run this test again?", array(
+ 'choices' => array('y', 'n'),
+ 'default' => 'y'
+ ));
+ if ($again == 'y') {
+ return $this->run();
+ }
+
+ $another = $this->in("Would you like to run another test?", array(
+ 'choices' => array('y', 'n'),
+ 'default' => 'y'
+ ));
+ if ($another == 'y') {
+ $this->case = $this->group = null;
+ return $this->run();
+ }
+ }
+ /**
+ * Shows which classes are un-tested
+ *
+ * @return void
+ */
+ public function missing() {
+ $tests = Group::all();
+ $this->header('Classes with no test case');
+ $classFilter = '/\w+Test$|webroot|index$|^app\\\\config|^app\\\\views/';
+ $classes = array_diff(
+ Libraries::find(true, array('exclude' => $classFilter, 'recursive' => true)),
+ $tests
+ );
+ sort($classes);
+ $this->out($classes);
+ }
+
+ /**
+ * Provide a list of test cases and accept input as case to run
+ *
+ * @return void
+ */
+ protected function _getTests() {
+ while (empty($this->case) && empty($this->group)) {
+ $tests = Libraries::find(true, array('filter' => '/\w+Test$/', 'recursive' => true));
+ $tests = str_replace('\\', '.',
+ str_replace('lithium\tests\cases\\', '', $tests)
+ );
+ foreach ($tests as $key => $test) {
+ $this->out(++$key . ". " . $test);
+ }
+ $number = $this->in("Choose a test case. (q to quit)");
+
+ if (isset($tests[--$number])) {
+ $this->case = $tests[$number];
+ }
+
+ if ($number == 'q') {
+ return 0;
+ }
+ }
+ return 1;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/docs/Generator.php b/libraries/lithium/console/commands/docs/Generator.php
new file mode 100644
index 0000000..726949a
--- /dev/null
+++ b/libraries/lithium/console/commands/docs/Generator.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands\docs;
+
+use \lithium\core\Libraries;
+use \lithium\util\reflection\Inspector;
+
+/**
+ * Adds headers and docblocks to classes and methods
+ *
+ **/
+class Generator extends \lithium\console\Command {
+
+ public function run() {
+ $classes = Libraries::find(true, array(
+ 'exclude' => "/webroot|index$|^app\\\\config|^app\\\\views/",
+ 'recursive' => true
+ ));
+
+ foreach($classes as $class) {
+ $path = Libraries::path($class);
+ $contents = explode("\n", file_get_contents($path));
+ $contents = $this->_header($contents);
+ if (file_put_contents($path, implode("\n", $contents))) {
+ $this->out($path . ' written');
+ }
+ }
+
+ }
+
+ protected function _header($contents) {
+ if (strpos($contents[1], '*') === false) {
+ $header = explode("\n", file_get_contents(
+ dirname(dirname(__DIR__)) . '/templates/docs/header.txt.php')
+ );
+ $one = array_shift($contents);
+ $contents = array_merge($header, $contents);
+ array_unshift($contents, $one);
+ }
+ return $contents;
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/docs/Todo.php b/libraries/lithium/console/commands/docs/Todo.php
new file mode 100644
index 0000000..7c3e03d
--- /dev/null
+++ b/libraries/lithium/console/commands/docs/Todo.php
@@ -0,0 +1,80 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands\docs;
+
+use \lithium\core\Libraries;
+use \lithium\console\Request;
+use \lithium\util\Inflector;
+use \lithium\util\reflection\Inspector;
+
+/**
+ * Searches and displays @todo, @discuss, @fix and @important comments in your code.
+ */
+class Todo extends \lithium\console\Command {
+
+ /**
+ * undocumented variable
+ *
+ * @var id
+ */
+ public $show;
+
+ public function main($dir = null) {
+ if (!$dir) {
+ $namespace = '\\';
+ } else {
+ $namespace = array_reduce(explode('/', $dir), function($namespace, $part) {
+ return $namespace . '\\' . Inflector::camelize($part);
+ });
+ }
+ $libs = Libraries::find($namespace, array('recursive' => true));
+ $files = array();
+
+ foreach ($libs as $lib) {
+ $file = Libraries::path($lib);
+ if ($matches = static::parse(file_get_contents($file))) {
+ $files[$file] = $matches;
+ }
+ }
+
+ foreach ($files as $file => $matches) {
+ if (!$this->show) {
+ Console::out($file.':');
+ }
+ $rows = array(array('', 'ID', 'LINE', 'TYPE', 'TEXT'));
+ foreach ($matches as $match) {
+ $id = substr(sha1($file.$match['line']), 0, 4);
+ if ($id == $this->show) {
+ Console::stop(0, $file);
+ }
+ $rows[] = array('', $id, $match['line'], $match['type'], $match['text']);
+ }
+ if (!$this->show) {
+ Console::out(Console::columns($rows));
+ Console::hr();
+ Console::nl();
+ }
+ }
+ }
+
+ /**
+ * undocumented
+ *
+ */
+}
+
+// if (1) {
+// Request::dispatch(new Request(compact('argv')), new Todo());
+// }
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/generate/Component.php b/libraries/lithium/console/commands/generate/Component.php
new file mode 100644
index 0000000..4a665b1
--- /dev/null
+++ b/libraries/lithium/console/commands/generate/Component.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands\bake;
+
+class Component {
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/generate/Controller.php b/libraries/lithium/console/commands/generate/Controller.php
new file mode 100644
index 0000000..8d347ab
--- /dev/null
+++ b/libraries/lithium/console/commands/generate/Controller.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands\bake;
+
+class Controller {
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/generate/Model.php b/libraries/lithium/console/commands/generate/Model.php
new file mode 100644
index 0000000..dd6c91d
--- /dev/null
+++ b/libraries/lithium/console/commands/generate/Model.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands\bake;
+
+class Model {
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/commands/generate/View.php b/libraries/lithium/console/commands/generate/View.php
new file mode 100644
index 0000000..c428e9e
--- /dev/null
+++ b/libraries/lithium/console/commands/generate/View.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\console\commands\bake;
+
+class View {
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/li3 b/libraries/lithium/console/li3
new file mode 100755
index 0000000..9a24ea1
--- /dev/null
+++ b/libraries/lithium/console/li3
@@ -0,0 +1,23 @@
+#!/bin/bash
+################################################################################
+#
+#
+# Lithium: the most rad php framework
+# Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+#
+# Licensed under The BSD License
+# Redistributions of files must retain the above copyright notice.
+#
+# @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+# @license http://opensource.org/licenses/bsd-license.php The BSD License
+#
+#
+################################################################################
+clear
+
+LIB=${0/%li3/}
+APP=`pwd`
+
+exec php -q ${LIB}lithium.php -working "${APP}" "$@"
+
+exit $?;
\ No newline at end of file
diff --git a/libraries/lithium/console/lithium.php b/libraries/lithium/console/lithium.php
new file mode 100644
index 0000000..374e649
--- /dev/null
+++ b/libraries/lithium/console/lithium.php
@@ -0,0 +1,55 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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.
+ */
+$up = function($dir) {
+ return (($parent = dirname($dir)) != $dir) ? $parent : false;
+};
+
+$current = function($pwd = null) use ($argc, $argv) {
+ return ($pwd = array_search('-working', $argv) && $argc > $pwd) ? $argv[$pwd + 1] : __DIR__;
+};
+
+$app = null;
+
+for ($dir = $current(); !$app && $dir; $dir = $up($dir)) {
+ if (is_dir($dir . '/config') && file_exists($dir . '/config/bootstrap.php')) {
+ $app = $dir;
+ }
+}
+
+if ($app) {
+ include $app . '/config/bootstrap.php';
+} else {
+ define('LITHIUM_LIBRARY_PATH', dirname(dirname(__DIR__)));
+ define('LITHIUM_APP_PATH', dirname(LITHIUM_LIBRARY_PATH) . '/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 .= "config/bootstrap.php. It should point to the directory containing your ";
+ $message .= "/libraries directory.";
+ trigger_error($message, E_USER_ERROR);
+ }
+ Libraries::add('lithium');
+}
+
+exit(Dispatcher::run());
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/console/templates/docs/header.txt.php b/libraries/lithium/console/templates/docs/header.txt.php
new file mode 100644
index 0000000..077346b
--- /dev/null
+++ b/libraries/lithium/console/templates/docs/header.txt.php
@@ -0,0 +1,10 @@
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
\ No newline at end of file
diff --git a/libraries/lithium/core/Adaptable.php b/libraries/lithium/core/Adaptable.php
new file mode 100644
index 0000000..65dec72
--- /dev/null
+++ b/libraries/lithium/core/Adaptable.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+use \lithium\util\Collection;
+
+class Adaptable extends \lithium\core\StaticObject {
+
+ /**
+ * To be re-defined in sub-classes.
+ *
+ * @var object Collection of configurations, indexed by name.
+ */
+ protected static $_configurations = null;
+
+ /**
+ * Initialization of static class
+ *
+ * @return void
+ */
+ public static function __init() {
+ return static::$_configurations = new Collection();
+ }
+
+ /**
+ * Sets configurations for a particular adaptable implementation, or returns
+ * the current configuration settings.
+ *
+ * @param array $config Configurations, indexed by name
+ * @return object Collection of configurations
+ */
+ public static function config($config = null) {
+ $default = array('adapter' => null, 'filters' => array());
+
+ if ($config) {
+ $items = array_map(function($i) use ($default) { return $i + $default; }, $config);
+ static::$_configurations = new Collection(compact('items'));
+ }
+ return static::$_configurations;
+ }
+
+ /**
+ * Clears configurations
+ *
+ * @return void
+ */
+ public static function reset() {
+ static::$_configurations = new Collection();
+ }
+
+ /**
+ * Returns adapter class name for given $name configuration
+ *
+ * @param string $library Dot-delimited location of library, in a format
+ * compatible with Libraries::locate().
+ * @param string $name Classname of adapter to load
+ * @return string Adapter object
+ */
+ protected static function _adapter($library, $name = null) {
+ $settings = static::$_configurations;
+
+ if (empty($name)) {
+ $names = $settings->keys();
+ if (empty($names)) {
+ return;
+ }
+ $name = end($names);
+ }
+
+ if (!isset($settings[$name])) {
+ return;
+ }
+
+ if (is_string($settings[$name]['adapter'])) {
+ $config = $settings[$name];
+
+ if (!$class = Libraries::locate($library, $config['adapter'])) {
+ return null;
+ }
+ $settings[$name] = array('adapter' => new $class($config)) + $settings[$name];
+ }
+
+ return $settings[$name]['adapter'];
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/Environment.php b/libraries/lithium/core/Environment.php
new file mode 100644
index 0000000..99cb2e0
--- /dev/null
+++ b/libraries/lithium/core/Environment.php
@@ -0,0 +1,94 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+use \lithium\util\Set;
+
+class Environment {
+
+ protected static $_configurations = array(
+ 'base' => array(),
+ 'production' => array(
+ 'inherit' => 'base'
+ ),
+ 'development' => array(
+ 'inherit' => 'base'
+ ),
+ 'test' => array(
+ 'inherit' => 'development'
+ )
+ );
+
+ protected static $_current = null;
+
+ protected static $_detector = null;
+
+ public static function is($detect = null) {
+ if (is_callable($detect)) {
+ static::$_detector = $detect;
+ } elseif (is_string($detect)) {
+ return (static::$_current == $detect);
+ }
+ return static::$_current;
+ }
+
+ public static function get($name = null, $path = null) {
+ if (empty($name) && empty($path)) {
+ return static::$_current;
+ }
+ if (!isset(static::$_configurations[$name])) {
+ return null;
+ }
+ return static::$_configurations[$name];
+ }
+
+ /**
+ * Creates, modifies or switches to an existing configuration.
+ *
+ * @param mixed $env
+ * @param array $config
+ * @return array
+ */
+ public static function set($env, $config = null) {
+ if (is_null($config)) {
+ switch(true) {
+ case is_object($env) || is_array($env):
+ static::$_current = static::_detector()->__invoke($env);
+ break;
+ case isset(static::$_configurations[$env]):
+ static::$_current = $env;
+ break;
+ }
+ return;
+ }
+
+ if (isset(static::$_configurations[$env]) && $base = static::$_configurations[$env]) {
+ return static::$_configurations[$env] = Set::merge($base, $config);
+ }
+ }
+
+ protected static function _detector() {
+ return static::$_detector ?: function($request) {
+ switch (true) {
+ case (in_array($request->env('SERVER_ADDR'), array('::1', '127.0.0.1'))):
+ return 'development';
+ case (preg_match('/^test/', $request->env('HTTP_HOST'))):
+ return 'test';
+ default:
+ return 'production';
+ }
+ };
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/Libraries.php b/libraries/lithium/core/Libraries.php
new file mode 100644
index 0000000..74911f3
--- /dev/null
+++ b/libraries/lithium/core/Libraries.php
@@ -0,0 +1,496 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+use \Exception;
+use \RecursiveIteratorIterator;
+use \RecursiveDirectoryIterator;
+use \lithium\util\String;
+
+class Libraries {
+
+ /**
+ * The list of class libraries registered with the class loader.
+ *
+ * @var array
+ */
+ protected static $_configurations = array();
+
+ /**
+ * Contains a cascading list of path templates, indexed by object type.
+ *
+ * @var array
+ */
+ protected static $_classPaths = array(
+ 'adapters' => array(
+ '{:library}\extensions\adapters\{:namespace}\{:class}\{:name}',
+ '{:library}\extensions\adapters\{:class}\{:name}',
+ '{:library}\{:namespace}\{:class}\adapters\{:name}' => array('libraries' => 'lithium')
+ ),
+ 'commands' => array(
+ '{:library}\extensions\commands\{:class}\{:name}',
+ '{:library}\extensions\commands\{:name}',
+ '{:library}\console\commands\{:name}' => array('libraries' => 'lithium')
+ ),
+ 'controllers' => array('{:library}\controllers\{:name}Controller'),
+ 'helpers' => array(
+ '{:library}\extensions\helpers\{:name}',
+ '{:library}\template\helpers\{:name}' => array('libraries' => 'lithium')
+ ),
+ 'models' => array('{:library}\models\{:name}'),
+ 'sockets' => array(
+ '{:library}\extensions\sockets\{:name}',
+ '{:library}\{:class}\socket\{:name}' => array('libraries' => 'lithium')
+ ),
+ 'testFilters' => array(
+ '{:library}\tests\filters\{:name}',
+ '{:library}\test\filters\{:name}' => array('libraries' => 'lithium')
+ )
+ );
+
+ protected static $_pluginPaths = array(
+ '{:app}/libraries/plugins/{:name}',
+ '{:root}/plugins/{:name}'
+ );
+
+ /**
+ * Holds cached class paths generated and used by lithium\core\Libraries::load().
+ *
+ * @var array
+ * @see lithium\core\Libraries::load()
+ */
+ protected static $_cachedPaths = array();
+
+ /**
+ * Adds a class library from which files can be loaded
+ *
+ * @param string $name Library name, i.e. 'app', 'lithium', 'pear' or 'solar'.
+ * @param array $options Specifies where the library is in the filesystem, and how classes
+ * should be loaded from it. Allowed keys are:
+ * - 'path': The directory containing the library.
+ * - 'loader': An auto-loader method associated with the library, if any
+ * - 'bootstrap': A file path (relative to 'path') to a bootstrap script that
+ * should be run when the library is added.
+ * - 'prefix': The class prefix this library uses, i.e. 'lithium\', 'Zend_'
+ * or 'Solar_'.
+ * - 'suffix': Gets tacked on 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
+ * 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.
+ * - 'defer': If true, indicates that, when locating classes, this library should
+ * defer to other libraries in order of preference.
+ * @return array Returns the resulting set of options created for this library.
+ */
+ public static function add($name, $config = array()) {
+ $defaults = array(
+ 'path' => LITHIUM_LIBRARY_PATH . '/' . $name,
+ 'loader' => null,
+ 'prefix' => $name . "\\",
+ 'suffix' => '.php',
+ 'transform' => null,
+ 'bootstrap' => null,
+ 'defer' => false
+ );
+
+ switch ($name) {
+ case 'app':
+ $defaults['path'] = LITHIUM_APP_PATH;
+ $defaults['bootstrap'] = 'config/switchboard.php';
+ break;
+ case 'lithium':
+ $defaults['loader'] = 'lithium\core\Libraries::load';
+ $defaults['defer'] = true;
+ break;
+ }
+
+ if ($name === 'plugin') {
+ return static::_addPlugins((array)$config);
+ }
+ static::$_configurations[$name] = ((array)$config += $defaults);
+
+ if (!empty($config['bootstrap'])) {
+ if ($config['bootstrap'] === true) {
+ $config['bootstrap'] = 'config/bootstrap.php';
+ }
+ require $config['path'] . '/' . $config['bootstrap'];
+ }
+
+ if (!empty($config['loader'])) {
+ spl_autoload_register($config['loader']);
+ }
+ return $config;
+ }
+
+ public static function get($name = null) {
+ if (empty($name)) {
+ return static::$_configurations;
+ }
+ return isset(static::$_configurations[$name]) ? static::$_configurations[$name] : null;
+ }
+
+ /**
+ * Removes a registered library, and unregister's the library's autoloader, if it has one.
+ *
+ * @param mixed $name A string or array of library names indicating the libraries you wish to
+ * remove, i.e. `'app'` or `'lithium'`. This can also be used to unload plugins by
+ * name.
+ * @return void
+ */
+ public static function remove($name) {
+ foreach ((array)$name as $library) {
+ if (isset(static::$_configurations[$library])) {
+ if (static::$_configurations[$library]['loader']) {
+ spl_autoload_unregister(static::$_configurations[$library]['loader']);
+ }
+ unset(static::$_configurations[$library]);
+ }
+ }
+ }
+
+ /**
+ * Finds the classes in a library/namespace/folder
+ *
+ * @todo Tie this into how path() is implemented
+ * @param string $library
+ * @param string $options
+ * @return array
+ */
+ public static function find($library, $options = array()) {
+ if ($library === true) {
+ $libs = array();
+
+ foreach (array_keys(static::$_configurations) as $library) {
+ $libs = array_merge($libs, static::find($library, $options));
+ }
+ return $libs;
+ }
+
+ $defaults = array(
+ 'recursive' => false,
+ 'filter' => '/^(\w+)?(\\\\[a-z0-9_]+)+\\\\[A-Z][a-zA-Z0-9]+$/',
+ 'exclude' => '',
+ 'path' => '',
+ 'format' => false,
+ 'namespaces' => false
+ );
+ $options += $defaults;
+
+ if ($options['namespaces'] && $options['filter'] == $defaults['filter']) {
+ $options['filter'] = false;
+ }
+
+ if (!isset(static::$_configurations[$library])) {
+ return null;
+ }
+ $config = static::$_configurations[$library];
+ $path = rtrim($config['path'] . $options['path'], '/');
+ $filter = '/^.+\/[A-Za-z0-9_]+$|^.*' . preg_quote($config['suffix'], '/') . '/';
+
+ $search = function($path) use ($filter, $options, $config) {
+ return preg_grep($filter, glob(
+ $path . '/*' . ($options['namespaces'] ? '' : $config['suffix'])
+ ));
+ };
+ $libs = $search($path);
+
+ if ($options['recursive']) {
+ $dirs = $queue = array_diff(glob($path . '/*', GLOB_ONLYDIR), $libs);
+
+ while ($queue) {
+ $dir = array_pop($queue);
+
+ if (!is_dir($dir)) {
+ continue;
+ }
+ $libs = array_merge($libs, $search($dir));
+ $queue = array_merge($queue, array_diff(glob($dir . '/*', GLOB_ONLYDIR), $libs));
+ }
+ }
+ $trim = array(strlen($config['path']) + 1, strlen($config['suffix']));
+
+ if ($options['format'] != 'files') {
+ foreach ($libs as $i => $file) {
+ $rTrim = strpos($file, $config['suffix']) !== false ? -$trim[1] : 9999;
+ $file = preg_split('/[\/\\\\]/', substr($file, $trim[0], $rTrim));
+ $libs[$i] = $config['prefix'] . join('\\', $file);
+ }
+ }
+
+ $exclude = $options['exclude'];
+ $libs = $exclude ? preg_grep($exclude, $libs, PREG_GREP_INVERT) : $libs;
+ $libs = $options['filter'] ? preg_grep($options['filter'], $libs) : $libs;
+ return array_values($libs);
+ }
+
+ /**
+ * Get the corresponding physical file path for a class name.
+ *
+ * @param string $class
+ * @param string $options
+ * @return array
+ */
+ public static function path($class, $options = array()) {
+ if (array_key_exists($class, static::$_cachedPaths)) {
+ return static::$_cachedPaths[$class];
+ }
+ $class = ($class[0] == '\\') ? substr($class, 1) : $class;
+
+ foreach (static::$_configurations as $name => $options) {
+ if (strpos($class, $options['prefix']) !== 0) {
+ continue;
+ }
+
+ if (!empty($options['transform'])) {
+ if (is_object($options['transform'])) {
+ return $options['transform']($class, $options);
+ }
+ list($match, $replace) = $options['transform'];
+ return preg_replace($match, $replace, $class);
+ }
+ $path = str_replace("\\", '/', substr($class, strlen($options['prefix'])));
+ return $options['path'] . '/' . $path . $options['suffix'];
+ }
+ }
+
+ /**
+ * Performs service location for an object of a specific type.
+ *
+ * @param string $type
+ * @param string $name
+ * @return void
+ */
+ public static function locate($type, $name = null, $options = array()) {
+ if (strpos($name, '\\') !== false) {
+ return $name;
+ }
+ $ident = $name ? $type . '.' . $name : $type;
+
+ if (isset(static::$_cachedPaths[$ident])) {
+ return static::$_cachedPaths[$ident];
+ }
+
+ if (strpos($type, '.')) {
+ $parts = explode('.', $type);
+ $type = array_shift($parts);
+
+ switch (count($parts)) {
+ case 1:
+ list($class) = $parts;
+ break;
+ case 1:
+ case 2:
+ list($namespace, $class) = $parts;
+ break;
+ default:
+ $class = array_pop($parts);
+ $namespace = join('\\', $parts);
+ break;
+ }
+ }
+
+ if (!isset(static::$_classPaths[$type])) {
+ return null;
+ }
+
+ if (is_null($name)) {
+ return static::_locateAll($type);
+ }
+
+ $params = compact('type', 'namespace', 'class', 'name');
+ $paths = static::$_classPaths[$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']
+ ));
+ 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);
+ }
+ }
+
+ /**
+ * Loads the class definition specified by `$class`. Also calls the __init() method on the
+ * class, if defined. Looks through the list of libraries defined in $_configurations, which
+ * are added through lithium\core\Libraries::add().
+ *
+ * @param string $class The fully-namespaced (where applicable) name of the class to load.
+ * @see lithium\core\Libraries::add()
+ * @see lithium\core\Libraries::path()
+ * @return void
+ */
+ public static function load($class, $require = false) {
+ if (($path = static::path($class)) && is_readable($path) && include $path) {
+ static::$_cachedPaths[$class] = $path;
+ method_exists($class, '__init') ? $class::__init() : null;
+ } elseif ($require) {
+ throw new Exception("Failed to load {$class} from {$path}");
+ }
+ }
+
+ protected static function _locateAll($type, $options = array()) {
+ $defaults = array('libraries' => null);
+ $options += $defaults;
+ $type = explode('.', $type);
+
+ $paths = $classes = array();
+ $pathTemplates = static::$_classPaths[current($type)];
+ $libraries = $options['libraries'] ?: array_keys(static::$_configurations);
+
+ foreach ($libraries as $library) {
+ $config = static::$_configurations[$library];
+
+ foreach ($pathTemplates as $template => $tplOpts) {
+ if (is_int($template)) {
+ $template = $tplOpts;
+ $tplOpts = array();
+ }
+ $scope = $options['libraries'] ? (array)$options['libraries'] : null;
+
+ if ($scope && !in_array($library, $scope)) {
+ continue;
+ }
+ $params['library'] = $config['path'];
+ $path = str_replace('\\', '/', preg_replace('/\\\{:\w+}/', '', String::insert(
+ $template, $params, array('escape' => '/')
+ )));
+
+ if (is_dir($path)) {
+ $paths[$path] = $library;
+ }
+ }
+ }
+
+ foreach ($paths as $path => $library) {
+ $config = static::$_configurations[$library];
+ $suffix = '/' . preg_quote($config['suffix'], '/') . '$/';
+ foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($path)) as $i) {
+ if ($i->isFile() && preg_match($suffix, $i->getFilename())) {
+ $trim = array(strlen($config['path']) + 1, strlen($config['suffix']));
+ $file = substr($i->getPathname(), $trim[0], -$trim[1]);
+ $classes[] = $config['prefix'] . str_replace('/', '\\', $file);
+ }
+ }
+ }
+ return $classes;
+ }
+
+ /**
+ * Performs service location lookups by library, based on the library's `'defer'` flag.
+ * Libraries with `'defer'` set to `true` will be searched last when looking up services.
+ *
+ * @param boolean $defer A boolean flag indicating which libraries to search, either the ones
+ * with the `'defer'` flag set, or the ones without.
+ * @param array $paths The list of paths to be searched for the given service (class). These
+ * are defined in `lithium\core\Libraries::$_classPaths`, and are organized by class
+ * type.
+ * @param array $params The list of insert parameters to be injected into each path format
+ * string when searching for classes.
+ * @param array $options
+ * @return string Returns a class path as a string if a given class is found, or null if no
+ * class in any path matching any of the parameters is located.
+ * @see lithium\core\Libraries::$_classPaths
+ * @see lithium\core\Libraries::locate()
+ */
+ protected static function _locateDeferred($defer, $paths, $params, $options = array()) {
+ 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;
+ }
+
+ foreach ($libraries as $library => $config) {
+ if ($config['defer'] !== $defer && $defer !== null) {
+ continue;
+ }
+
+ foreach ($paths as $pathTemplate => $options) {
+ if (is_int($pathTemplate)) {
+ $pathTemplate = $options;
+ $options = array();
+ }
+ $scope = isset($options['libraries']) ? (array)$options['libraries'] : null;
+
+ if ($scope && !in_array($library, $scope)) {
+ continue;
+ }
+ $params['library'] = $library;
+ $classPath = String::insert($pathTemplate, $params);
+
+ if (file_exists(Libraries::path($classPath))) {
+ return $classPath;
+ }
+ }
+ }
+ }
+
+ /**
+ * Register a Lithium plugin
+ *
+ * @param string $plugins
+ * @param string $options
+ * @return void
+ */
+ protected static function _addPlugins($plugins) {
+ $defaults = array('bootstrap' => null, 'route' => true);
+ $params = array('app' => LITHIUM_APP_PATH, 'root' => LITHIUM_LIBRARY_PATH);
+ $result = array();
+
+ foreach ($plugins as $name => $options) {
+ if (is_int($name)) {
+ $name = $options;
+ $options = array();
+ }
+ $params = compact('name') + $params;
+
+ if (!isset($options['path'])) {
+ foreach (static::$_pluginPaths as $path) {
+ if (is_dir($dir = String::insert($path, $params))) {
+ $options['path'] = $dir;
+ break;
+ }
+ }
+ }
+ $plugin = static::add($name, $options + $defaults);
+
+ if ($plugin['route']) {
+ $defaultRoutes = $plugin['path'] . '/config/routes.php';
+ $route = ($plugin['route'] === true) ? $defaultRoutes : $plugin['route'];
+ !file_exists($route) ?: include $route;
+ }
+ $result[$name] = $plugin;
+ }
+ return $result;
+ }
+}
+
+if (!defined('LITHIUM_LIBRARY_PATH')) {
+ define('LITHIUM_LIBRARY_PATH', dirname(__DIR__));
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/Object.php b/libraries/lithium/core/Object.php
new file mode 100644
index 0000000..7323a4b
--- /dev/null
+++ b/libraries/lithium/core/Object.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\core;
+
+use \lithium\util\collection\Filters;
+
+/**
+ * Base class in Lithium hierarchy, from which all other dynamic classes inherit.
+ *
+ * @package Lithium
+ */
+class Object {
+
+ /**
+ * Stores configuration information for object instances at time of construction.
+ * **Do not override.** Pass any additional variables to `parent::__construct()`.
+ *
+ * @var array
+ */
+ protected $_config = array();
+
+ protected $_autoConfig = array();
+
+ protected $_methodFilters = array();
+
+ protected $_extendMethodFilters = array();
+
+ protected static $_parents = array();
+
+ public function __construct($config = array()) {
+ $defaults = array('init' => true);
+ $this->_config = (array)$config + $defaults;
+
+ if ($this->_config['init']) {
+ $this->_init();
+ }
+ }
+
+ /**
+ * Initializer function. Called by constructor unless constructor `'init'` flag set to false.
+ * May be used for testing purposes, where objects need to be manipulated in an un-initialized
+ * state.
+ *
+ * @return void
+ */
+ protected function _init() {
+ if ($this->_autoConfig === array(true)) {
+ $this->_autoConfig = array_keys($this->_config);
+ }
+
+ foreach ($this->_autoConfig as $key => $flag) {
+ if (is_numeric($key)) {
+ $key = $flag;
+ $flag = null;
+ }
+
+ if (!array_key_exists($key, $this->_config)) {
+ continue;
+ }
+
+ switch ($flag) {
+ case 'merge':
+ $this->{"_$key"} = $this->_config[$key] + $this->{"_$key"};
+ break;
+ case 'call':
+ $this->{$key}($this->_config[$key]);
+ break;
+ default:
+ $this->{"_$key"} = $this->_config[$key];
+ break;
+ }
+ }
+ }
+
+ /**
+ * 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 clousure that is used to filter the method(s).
+ * @return void
+ * @see lithium\core\Object::_filter()
+ * @see lithium\util\collection\Filters
+ */
+ public function applyFilter($method, $closure = null) {
+ foreach ((array)$method as $m) {
+ if (!isset($this->_methodFilters[$m])) {
+ $this->_methodFilters[$m] = array();
+ }
+ $this->_methodFilters[$m][] = $closure;
+ }
+ }
+
+ /**
+ * Calls a method on this object with the given parameters. Provides an OO wrapper
+ * for call_user_func_array, and improves performance by using straight method calls
+ * in most cases.
+ *
+ * @param string $method Name of the method to call
+ * @param array $params Parameter list to use when calling $method
+ * @return mixed Returns the result of the method call
+ */
+ public function invokeMethod($method, $params = array()) {
+ switch (count($params)) {
+ case 0:
+ return $this->{$method}();
+ case 1:
+ return $this->{$method}($params[0]);
+ case 2:
+ return $this->{$method}($params[0], $params[1]);
+ case 3:
+ return $this->{$method}($params[0], $params[1], $params[2]);
+ case 4:
+ return $this->{$method}($params[0], $params[1], $params[2], $params[3]);
+ case 5:
+ return $this->{$method}($params[0], $params[1], $params[2], $params[3], $params[4]);
+ default:
+ return call_user_func_array(array(&$this, $method), $params);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param string|array $method The name of the method being executed, or an array containing
+ * the name of the class that defined the method, and the method name.
+ * @param array $params An associative array containing all the parameters passed into
+ * the method.
+ * @param Closure $callback The method's implementation, wrapped in a closure.
+ * @param array $filters Additional filters to apply to the method for this call only
+ * @return mixed
+ */
+ protected function _filter($method, $params, $callback, $filters = array()) {
+ $class = null;
+
+ if (strpos($method, '::')) {
+ list($class, $method) = explode('::', $method);
+ }
+ $items = array($callback);
+
+ if (empty($this->_methodFilters[$method]) && empty($filters)) {
+ $chain = new Filters(compact('items', 'class', 'method'));
+ return $callback->__invoke($this, $params, $chain);
+ }
+
+ $items = array_merge($this->_methodFilters[$method], $filters, array($callback));
+ $chain = new Filters(compact('items', 'class', 'method'));
+
+ $start = $chain->rewind();
+ return $start($this, $params, $chain);
+ }
+
+ 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.
+ *
+ * @return void
+ */
+ protected function _stop() {
+ exit();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/core/StaticObject.php b/libraries/lithium/core/StaticObject.php
new file mode 100644
index 0000000..e32d01e
--- /dev/null
+++ b/libraries/lithium/core/StaticObject.php
@@ -0,0 +1,198 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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 \SplStack;
+
+/**
+ * Alternative base class in Lithium hierarchy, from which all (and only) static classes inherit.
+ *
+ * @package Lithium
+ */
+class StaticObject {
+
+ /**
+ * Stores configuration information for object instances at time of construction.
+ * **Do not override.** Pass any additional variables to `parent::__construct()`.
+ *
+ * @var array
+ */
+ protected static $_config = array();
+
+ protected static $_methodFilters = array();
+
+ protected static $_extendMethodFilters = array();
+
+ public static function __init() {}
+
+ /**
+ * Apply a closure to a method of the current static object.
+ *
+ * @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 clousure that is used to filter the method.
+ * @return void
+ * @see lithium\core\StaticObject::_filter()
+ * @see lithium\util\collection\Filters
+ */
+ public static function applyFilter($method, $closure = null) {
+ foreach ((array)$method as $m) {
+ if (!isset(static::$_methodFilters[$m])) {
+ static::$_methodFilters[$m] = array();
+ }
+ static::$_methodFilters[$m][] = $closure;
+ }
+ }
+
+ /**
+ * Applies the configured strategies to a method of the current static object.
+ *
+ * @param string $method The strategy method to be called.
+ * @param array $params Parameters that are used by the strategy $method.
+ * @return mixed Data that has been modified by the configured strategies.
+ **/
+ public static function applyStrategies($method, $params = array()) {
+ $strategies = self::strategies($params['name']);
+
+ //switch ($method) {
+ //case 'read':
+ //$mode = SplStack::IT_MODE_LIFO | SplStack::IT_MODE_KEEP;
+ //break;
+ //case 'write':
+ //$mode = SplStack::IT_MODE_FIFO | SplStack::IT_MODE_KEEP;
+ //break;
+ //}
+
+ //$strategies->setIteratorMode($mode);
+ foreach ($strategies as $strategy) {
+ $strategy::$method($params);
+ }
+
+ return $params['data'];
+ }
+
+ /**
+ * Allows setting & querying of static object strategies.
+ *
+ *
+ * - If $name is set, returns the strategies attached to the current static object.
+ * - If $name and $strategy are set, $strategy is added to the strategy
+ * stack denoted by $name.
+ * - If $name and $strategy are not set, then the full
+ * indexed strategies array is returned (note: the strategies are wraped in
+ * \SplStack).
+ *
+ * @param string $name Name of cache configuration.
+ * @param string|array $strategy Fully namespaced cache strategy identifier.
+ * @return mixed See above description.
+ * @access public
+ */
+ public static function strategies($name = '', $strategy = null) {
+ if (empty($name)) {
+ return static::$_strategies;
+ }
+
+ if (!isset(static::$_strategies[$name])) {
+ static::$_strategies[$name] = new SplStack();
+ }
+
+ if (!empty($strategy)) {
+ $strategies = static::$_strategies[$name];
+
+ if (is_array($strategy)) {
+ array_walk($strategy, function($value) use (&$strategies) {
+ $strategies->push($value);
+ });
+ }
+ else if (is_string($strategy)) {
+ $strategies->push($strategy);
+ }
+
+ return true;
+ }
+
+ return static::$_strategies[$name];
+ }
+ /**
+ * Calls a method on this object with the given parameters. Provides an OO wrapper
+ * for call_user_func_array, and improves performance by using straight method calls
+ * in most cases.
+ *
+ * @param string $method Name of the method to call
+ * @param array $params Parameter list to use when calling $method
+ * @return mixed Returns the result of the method call
+ */
+ public static function invokeMethod($method, $params = array()) {
+ switch (count($params)) {
+ case 0:
+ return static::$method();
+ case 1:
+ return static::$method($params[0]);
+ case 2:
+ return static::$method($params[0], $params[1]);
+ case 3:
+ return static::$method($params[0], $params[1], $params[2]);
+ case 4:
+ return static::$method($params[0], $params[1], $params[2], $params[3]);
+ case 5:
+ return static::$method($params[0], $params[1], $params[2], $params[3], $params[4]);
+ default:
+ return call_user_func_array(array(get_called_class(), $method), $params);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @param string|array $method The name of the method being executed, or an array containing
+ * the name of the class that defined the method, and the method name.
+ * @param array $params An associative array containing all the parameters passed into
+ * the method.
+ * @param Closure $callback The method's implementation, wrapped in a closure.
+ * @param array $filters Additional filters to apply to the method for this call only
+ * @return mixed
+ */
+ protected static function _filter($method, $params, $callback, $filters = array()) {
+ $class = null;
+
+ if (strpos($method, '::')) {
+ list($class, $method) = explode('::', $method);
+ }
+
+ if (empty(static::$_methodFilters[$method]) && empty($filters)) {
+ return $callback->__invoke(get_called_class(), $params, null);
+ }
+
+ $chain = new Filters(array(
+ 'items' => array_merge(static::$_methodFilters[$method], $filters, array($callback)),
+ 'class' => $class,
+ 'method' => $method
+ ));
+
+ $start = $chain->rewind();
+ return $start(get_called_class(), $params, $chain);
+ }
+
+ /**
+ * Exit immediately. Primarily used for overrides during testing.
+ *
+ * @return void
+ */
+ protected static function _stop() {
+ exit();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/Connections.php b/libraries/lithium/data/Connections.php
new file mode 100644
index 0000000..59ad464
--- /dev/null
+++ b/libraries/lithium/data/Connections.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data;
+
+use \lithium\util\String;
+use \lithium\util\Collection;
+
+class Connections extends \lithium\core\Object {
+
+ protected static $_configurations = null;
+
+ protected static $_connections = null;
+
+ public static function __init() {
+ static::$_connections = new Collection();
+ static::$_configurations = new Collection();
+ require LITHIUM_APP_PATH . '/config/connections.php';
+ }
+
+ public static function add($name, $type = null, $config = array()) {
+ if (is_array($type)) {
+ list($config, $type) = array($type, null);
+ }
+ $defaults = array(
+ 'type' => $type ?: 'Database',
+ 'adapter' => null,
+ 'host' => 'localhost',
+ 'login' => '',
+ 'password' => ''
+ );
+ return static::$_configurations[$name] = (array)$config + $defaults;
+ }
+
+ public static function get($name = null, $options = array()) {
+ $defaults = array('config' => false, 'autoBuild' => true);
+ $options += $defaults;
+
+ if (empty($name)) {
+ return static::$_configurations->keys();
+ }
+
+ if (!isset(static::$_configurations[$name])) {
+ return null;
+ }
+
+ if ($options['config']) {
+ return static::$_configurations[$name];
+ }
+
+ if (!isset(static::$_connections[$name]) && $options['autoBuild']) {
+ return static::$_connections[$name] = static::_build(static::$_configurations[$name]);
+ } elseif (!$options['autoBuild']) {
+ return null;
+ }
+ return static::$_connections[$name];
+ }
+
+ /**
+ * clear connections and configurations
+ *
+ * @return void
+ */
+ public static function clear() {
+ static::$_connections = new Collection();
+ static::$_configurations = new Collection();
+ }
+
+ /**
+ * Constructs a DataSource object or adapter object instance from a configuration array.
+ *
+ * @param array $config
+ * @return object
+ * @todo Refactor class paths into lithium\core\Libraries
+ */
+ protected static function _build($config) {
+ $path = 'lithium\data\source\{:type}' . ($config['adapter'] ? '\adapter\{:adapter}' : '');
+ $class = String::insert($path, $config);
+ return new $class($config);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/Model.php b/libraries/lithium/data/Model.php
new file mode 100644
index 0000000..6b7d4db
--- /dev/null
+++ b/libraries/lithium/data/Model.php
@@ -0,0 +1,405 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+/**
+ * Model class
+ *
+ * @package default
+ * @todo Methods: bind(), and 'bind' option for find() et al., create(), save(), delete(),
+ * validate()
+ */
+class Model extends \lithium\core\StaticObject {
+
+ public $hasOne = array();
+
+ public $hasMany = array();
+
+ public $belongsTo = array();
+
+ protected static $_instances = array();
+
+ protected $_instanceFilters = array();
+
+ /**
+ * Class dependencies.
+ *
+ * @var array
+ */
+ protected $_classes = array(
+ 'query' => '\lithium\data\model\Query',
+ 'record' => '\lithium\data\model\Record',
+ 'validator' => '\lithium\util\Validator',
+ 'recordSet' => '\lithium\data\model\RecordSet',
+ 'connections' => '\lithium\data\Connections'
+ );
+
+ protected $_relations = array();
+
+ protected $_relationTypes = array(
+ 'belongsTo' => array('class', 'key', 'conditions', 'fields'),
+ 'hasOne' => array('class', 'key', 'conditions', 'fields', 'dependent'),
+ 'hasMany' => array(
+ 'class', 'key', 'conditions', 'fields', 'order', 'limit',
+ 'dependent', 'exclusive', 'finder', 'counter'
+ )
+ );
+
+ protected $_meta = array(
+ 'key' => 'id',
+ 'name' => null,
+ 'class' => null,
+ 'title' => null,
+ 'source' => null,
+ 'prefix' => null,
+ 'connection' => 'default'
+ );
+
+ protected $_schema = array();
+
+ /**
+ * Default query parameters.
+ *
+ * @var array
+ */
+ protected $_query = array(
+ 'conditions' => null,
+ 'fields' => null,
+ 'order' => null,
+ 'limit' => null,
+ 'page' => null
+ );
+
+ /**
+ * Custom find query properties, indexed by name.
+ */
+ protected $_finders = array();
+
+ /**
+ * Called when a model class is loaded. Used to call the default initialization routine.
+ *
+ * @return void
+ */
+ public static function __init() {
+ if (get_called_class() == __CLASS__) {
+ return;
+ }
+ static::init();
+ }
+
+ /**
+ * Sets default connection options and connect default finders.
+ *
+ * @return void
+ * @todo Merge in inherited config from AppModel and other parent classes.
+ */
+ public static function init($options = array()) {
+ $self = static::_instance();
+ $vars = get_class_vars(__CLASS__);
+ $base = $self->_meta + $vars['_meta'];
+ $self->_meta = (
+ $options + array('class' => get_called_class(), 'name' => static::_name()) + $base
+ );
+
+ if (empty($self->_meta['source']) && $self->_meta['source'] !== false) {
+ $self->_meta['source'] = Inflector::tableize($self->_meta['name']);
+ }
+
+ if (empty($meta['title'])) {
+ foreach (array('title', 'name', $self->_meta['key']) as $field) {
+ if (static::schema($field)) {
+ $self->_meta['title'] = $field;
+ break;
+ }
+ }
+ }
+ static::_instance()->_relations = static::_relations();
+ }
+
+ public static function __callStatic($method, $params) {
+ 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']);
+ }
+ }
+
+ /**
+ * undocumented function
+ *
+ * @param string $type
+ * @param string $options
+ * @return void
+ * @filter
+ */
+ public static function find($type, $options = array()) {
+ $self = static::_instance();
+ $classes = $self->_classes;
+
+ $defaults = array(
+ 'conditions' => null, 'fields' => null, 'order' => null, 'limit' => null, 'page' => 1
+ );
+
+ if (is_numeric($type) || $classes['validator']::isUuid($type)) {
+ $options['conditions'] = array(
+ "{$self->_meta['name']}.{$self->_meta['key']}" => $type
+ );
+ $type = 'first';
+ }
+
+ $options += ($self->_query + $defaults + compact('classes'));
+ $meta = array('meta' => $self->_meta, 'name' => get_called_class());
+ $params = compact('type', 'options');
+
+ return static::_filter(__METHOD__, $params, function($self, $params, $chain) use ($meta) {
+ $options = $params['options'] + array('model' => $meta['name']);
+ $connections = $options['classes']['connections'];
+ $name = $meta['meta']['connection'];
+
+ $query = new $options['classes']['query']($options);
+ $connection = $connections::get($name);
+
+ return new $options['classes']['recordSet'](array(
+ 'query' => $query,
+ 'model' => $options['model'],
+ 'handle' => &$connection,
+ 'classes' => $options['classes'],
+ 'resource' => $connection->read($query, array('return' => 'resource') + $options)
+ ));
+ });
+ }
+
+ /**
+ * 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
+ */
+ public static function finder($name, $options = null) {
+ $self = static::_instance();
+
+ if (empty($options)) {
+ return isset($self->_finders[$name]) ? $self->_finders[$name] : null;
+ }
+ $self->_finders[$name] = $options;
+ }
+
+ public static function meta($key = null, $value = null) {
+ $self = static::_instance();
+
+ if (!empty($value)) {
+ $self->_meta[$key] = $value;
+ return $self->_meta;
+ }
+ if (is_array($key)) {
+ $self->_meta = $key + $self->_meta;
+ }
+ if (is_array($key) || empty($key)) {
+ return $self->_meta;
+ }
+ return isset($self->_meta[$key]) ? $self->_meta[$key] : null;
+ }
+
+ public static function key($values = array()) {
+ $key = static::_instance()->_meta['key'];
+ $values = is_object($values) ? $values->to('array') : $values;
+
+ if (empty($values)) {
+ return $key;
+ }
+ if (is_array($key)) {
+ $scope = array_combine($key, array_fill(0, count($key), null));
+ return array_intersect_key($values, $scope);
+ }
+ return isset($values[$key]) ? $values[$key] : null;
+ }
+
+ public static function relations($name = null) {
+ $self = static::_instance();
+
+ if (empty($name)) {
+ return array_keys($self->_relations);
+ }
+
+ if (array_key_exists($name, $self->_relationTypes)) {
+ return $self->$name;
+ }
+
+ foreach (array_keys($self->_relationTypes) as $type) {
+ if (isset($self->{$type}[$name])) {
+ return $self->{$type}[$name];
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Lazy-initialize the schema for this Model object, if it is not already manually set in the
+ * object. You can declare `protected static $_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 with containing all fields is returned.
+ * @return array
+ */
+ public static function schema($field = null) {
+ $self = static::_instance();
+
+ if (empty($self->_schema)) {
+ $name = $self->_meta['connection'];
+ $conn = $self->_classes['connections'];
+ $self->_schema = $conn::get($name)->describe($self->_meta['source'], $self->_meta);
+ }
+ if (!empty($field)) {
+ return isset($self->_schema[$field]) ? $self->_schema[$field] : null;
+ }
+ return $self->_schema;
+ }
+
+ public static function hasField($field) {
+ if (is_array($field)) {
+ foreach ($field as $f) {
+ if (static::hasField($f)) {
+ return $f;
+ }
+ }
+ return false;
+ }
+ $schema = static::schema();
+ return (!empty($schema) && isset($schema[$field]));
+ }
+
+ protected static function _name() {
+ static $name;
+ return $name ?: $name = join('', array_slice(explode("\\", get_called_class()), -1));
+ }
+
+ /**
+ * This is pretty much completely broken right now
+ *
+ * @param string $type
+ * @param string $data
+ * @param string $altType
+ * @param string $r
+ * @param string $root
+ * @return void
+ */
+ protected static function _normalize($type, $data, $altType = null, $r = array(), $root = true) {
+
+ foreach ((array)$data as $name => $children) {
+ if (is_numeric($name)) {
+ $name = $children;
+ $children = array();
+ }
+
+ if (strpos($name, '.') !== false) {
+ $chain = explode('.', $name);
+ $name = array_shift($chain);
+ $children = array(join('.', $chain) => $children);
+ }
+
+ if (!empty($children)) {
+ if (get_called_class() == $name) {
+ $r = array_merge($r, static::_normalize($type, $children, $altType, $r, false));
+ } else {
+ if (!$_this->getAssociated($name)) {
+ $r[$altType][$name] = $children;
+ } else {
+ $r[$name] = static::_normalize($type, $children, $altType, @$r[$name], $_this->{$name});
+ }
+ }
+ } else {
+ if ($_this->getAssociated($name)) {
+ $r[$name] = array($type => null);
+ } else {
+ if ($altType != null) {
+ $r[$type][] = $name;
+ } else {
+ $r[$type] = $name;
+ }
+ }
+ }
+ }
+
+ if ($root) {
+ return array($this->name => $r);
+ }
+ return $r;
+ }
+
+ /**
+ * Re-implements `applyFilter()` from `StaticObject` to account for object instances.
+ *
+ * @see lithium\core\StaticObject::applyFilter()
+ */
+ public static function applyFilter($method, $closure = null) {
+ foreach ((array)$method as $m) {
+ if (!isset(static::_instance()->_instanceFilters[$m])) {
+ static::_instance()->_instanceFilters[$m] = array();
+ }
+ static::_instance()->_instanceFilters[$m][] = $closure;
+ }
+ }
+
+ protected static function _filter($method, $params, $callback, $filters = array()) {
+ $m = $method;
+ if (strpos($method, '::') !== false) {
+ list(, $m) = explode('::', $method, 2);
+ }
+ if (isset(static::_instance()->_instanceFilters[$m])) {
+ $filters = array_merge(static::_instance()->_instanceFilters[$m], $filters);
+ }
+ return parent::_filter($method, $params, $callback, $filters);
+ }
+
+ protected static function &_instance() {
+ $class = get_called_class();
+
+ if (!isset(static::$_instances[$class])) {
+ static::$_instances[$class] = new $class();
+ }
+ return static::$_instances[$class];
+ }
+
+ /**
+ * Iterates through relationship types to construct relation map.
+ *
+ * @return void
+ * @todo See if this can be rewritten to be lazy.
+ */
+ protected static function _relations() {
+ $relations = array();
+ $self = static::_instance();
+
+ 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;
+ }
+ }
+ return $relations;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/Source.php b/libraries/lithium/data/Source.php
new file mode 100644
index 0000000..3ed4118
--- /dev/null
+++ b/libraries/lithium/data/Source.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data;
+
+abstract class Source extends \lithium\core\Object {
+
+ protected $_connection = null;
+
+ protected $_isConnected = false;
+
+ public function __construct($config = array()) {
+ $defaults = array('autoConnect' => true);
+ parent::__construct((array)$config + $defaults);
+ }
+
+ public function __destruct() {
+ if ($this->_isConnected) {
+ $this->disconnect();
+ }
+ }
+
+ protected function _init() {
+ if ($this->_config['autoConnect']) {
+ $this->connect();
+ }
+ }
+
+ abstract public function connect();
+
+ abstract public function disconnect();
+
+ abstract public function entities($class = null);
+
+ abstract public function describe($entity, $meta = array());
+
+ abstract public function create($record, $options);
+
+ abstract public function read($query, $options);
+
+ /**
+ * Updates a set of records in a concrete data store.
+ *
+ * @param mixed $query An object which defines the update operation(s) that should be performed
+ * against the data store. This can be a `Query`, a `RecordSet`, a `Record`, or a
+ * subclass of one of the three. Alternatively, `$query` can be an
+ * adapter-specific query string.
+ * @param array $options Options to execute, which are defined by the concrete implementation.
+ * @return boolean Returns true if the update operation was a success, otherwise false.
+ */
+ abstract public function update($query, $options);
+
+ abstract public function delete($query, $options);
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/model/Query.php b/libraries/lithium/data/model/Query.php
new file mode 100644
index 0000000..a68faf7
--- /dev/null
+++ b/libraries/lithium/data/model/Query.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\model;
+
+class Query extends \lithium\core\Object {
+
+ protected $_model = null;
+
+ protected $_table = null;
+
+ /**
+ * The set of conditions that define the query's scope.
+ *
+ * @var array
+ * @see lithium\data\model\Query::conditions()
+ */
+ protected $_conditions = array();
+
+ protected $_fields = array();
+
+ protected $_order = null;
+
+ protected $_limit = null;
+
+ protected $_offset = null;
+
+ protected $_page = null;
+
+ protected $_joins = array();
+
+ protected $_comment = null;
+
+ protected function _init() {
+ foreach ($this->_config as $key => $val) {
+ if (method_exists($this, $key)) {
+ $this->{$key}($val);
+ }
+ }
+ }
+
+ public function model($model = null) {
+ if (empty($model)) {
+ return $this->_model;
+ }
+ $this->_model = $model;
+ $this->_table = $model::meta('source');
+ }
+
+ public function conditions($conditions = null) {
+ if (empty($conditions)) {
+ return $this->_conditions;
+ }
+ $this->_conditions = array_merge($this->_conditions, (array)$conditions);
+ }
+
+ public function fields($fields = null) {
+ if (empty($fields)) {
+ return $this->_fields;
+ }
+
+ if (is_array($fields)) {
+ $this->_fields = array_merge($this->_fields, $fields);
+ } else {
+ $this->_fields[] = $fields;
+ }
+ }
+
+ public function limit($limit = null) {
+ if (empty($limit)) {
+ return $this->_limit;
+ }
+ $this->_limit = intval($limit);
+ }
+
+ public function offset($offset = null) {
+ if (empty($offset)) {
+ return $this->_offset;
+ }
+ $this->_offset = intval($offset);
+ }
+
+ public function page($page = null) {
+ if (empty($page)) {
+ return $this->_page;
+ }
+ $this->_page = intval($page) ?: 1;
+ $this->offset(($this->_page - 1) * $this->_limit);
+ }
+
+ public function order($order = null) {
+ if (empty($order)) {
+ return $this->_order;
+ }
+ $this->_order = $order;
+ }
+
+ public function comment($comment = null) {
+ if (empty($comment)) {
+ preg_match('/^\s*\/\*\s(.+)\s\*\/$/', $this->_comment, $match);
+ return isset($match[1]) ? $match[1] : null;
+ }
+ $this->_comment = " /* {$comment} */";
+ }
+
+ public function export($dataSource) {
+ $results = array();
+
+ foreach (array('conditions', 'fields', 'order', 'limit') as $item) {
+ $results[$item] = $dataSource->{$item}($this->{$item}(), $this);
+ }
+ $results['table'] = $dataSource->name($this->_table);
+
+ foreach (array('comment', 'model') as $item) {
+ $results[$item] = $this->{'_' . $item};
+ }
+ return $results;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/model/Record.php b/libraries/lithium/data/model/Record.php
new file mode 100644
index 0000000..aa1fe71
--- /dev/null
+++ b/libraries/lithium/data/model/Record.php
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\model;
+
+class Record extends \lithium\core\Object {
+
+ protected $_model = null;
+
+ protected $_data = array();
+
+ protected $_autoConfig = array('model', 'data' => 'merge');
+
+ public function __construct($config = array()) {
+ $defaults = array('model' => null, 'data' => array());
+ parent::__construct((array)$config + $defaults);
+ }
+
+ public function __get($name) {
+ return isset($this->_data[$name]) ? $this->_data[$name] : null;
+ }
+
+ public function __isset($name) {
+ return array_key_exists($name, $this->_data);
+ }
+
+ public function to($format, $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/RecordSet.php b/libraries/lithium/data/model/RecordSet.php
new file mode 100644
index 0000000..adf4bbd
--- /dev/null
+++ b/libraries/lithium/data/model/RecordSet.php
@@ -0,0 +1,303 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\model;
+
+class RecordSet extends \lithium\util\Collection {
+
+ protected
+
+ /**
+ * 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.
+ *
+ * @var string
+ */
+ $_model = null,
+
+ /**
+ * An array containing each record's unique key. This allows, for example, lookups of
+ * records with composite keys, i.e.:
+ *
+ * {{{
+ * $payment = $records[array('client_id' => 42, 'invoice_id' => 21)];
+ * }}}
+ *
+ * @var array
+ */
+ $_index = array(),
+
+ $_pointer = 0,
+
+ /**
+ * 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
+ */
+ $_handle = null,
+
+ /**
+ * A reference to the query object that originated this record set; usually an instance of
+ * `lithium\data\model\Query`.
+ *
+ * @var object
+ */
+ $_query = null,
+
+ /**
+ * A pointer or resource that is used to load records from the object (`$_handle`) that
+ * originated this record set.
+ *
+ * @var resource
+ */
+ $_resource = null,
+
+ /**
+ * A 2D array of
+ *
+ * @var array
+ */
+ $_columns = array(),
+
+ $_classes = array(
+ 'record' => '\lithium\model\Record',
+ 'media' => '\lithium\http\Media'
+ ),
+
+ $_valid = true,
+
+ $_hasInitialized = false,
+
+ $_autoConfig = array('items', 'classes' => 'merge', 'handle', 'model', 'resource', 'query');
+
+ /**
+ * 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\model\RecordSet::$_columns
+ * @todo The part that uses _handle->columns() should be rewritten so that the column list
+ * is coming from the query object.
+ */
+ protected function _init() {
+ parent::_init();
+
+ if ($this->_handle && $this->_resource) {
+ $this->_columns = $this->_handle->columns($this->_query, $this->_resource, $this);
+ }
+ }
+
+ /**
+ * Checks to see if a record with the given index key is in the record set. If the record
+ * cannot be found, and not all records have been loaded into the set, it will continue loading
+ * records until either all available records have been loaded, or a matching key has been
+ * found.
+ *
+ * @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\model\RecordSet::offsetGet()
+ */
+ public function offsetExists($offset) {
+ if (in_array($offset, $this->_index)) {
+ return true;
+ }
+ return ($this->offsetGet($offset) !== null);
+ }
+
+ /**
+ * Gets a record from the record set using PHP's array syntax, i.e. `$records[5]`. Using loose
+ * typing, integer keys can be accessed using strings and vice-versa. For record sets with
+ * composite keys, records may be accessed using arrays as array keys. Note that the order of
+ * the keys in the array does not matter.
+ *
+ * Because record data in `RecordSet` is lazy-loaded from the database, new records are fetched
+ * until one with a matching key is found.
+ *
+ * @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\model\RecordSet::$_index
+ */
+ public function offsetGet($offset) {
+ if (!is_null($offset) && in_array($offset, $this->_index)) {
+ return $this->_items[array_search($offset, $this->_index)];
+ }
+ if ($this->_closed()) {
+ return null;
+ }
+ $model = $this->_model;
+
+ while ($record = $this->_populate(null, $offset)) {
+ if (!is_null($offset) && $offset == $model::key($record)) {
+ return $record;
+ }
+ }
+ $this->_close();
+ }
+
+ public function offsetSet($offset, $value) {
+ if (in_array($offset, $this->_index)) {
+ return $this->_items[array_search($offset, $this->_index)] = $value;
+ }
+ $this->_index[] = $offset;
+ return $this->_items[] = $value;
+ }
+
+ public function offsetUnset($offset) {
+ unset($this->_index[$index = array_search($offset, $this->_index)]);
+ unset($this->_items[$index]);
+ }
+
+ public function rewind() {
+ $this->_pointer = 0;
+ $this->_valid = (reset($this->_items) !== false && reset($this->_index));
+
+ if (!$this->_valid && !$this->_hasInitialized) {
+ $this->_hasInitialized = true;
+
+ if ($record = $this->_populate()) {
+ $this->_valid = true;
+ return $record;
+ }
+ }
+ return $this->_items[$this->_pointer];
+ }
+
+ public function current() {
+ return $this->_items[$this->_pointer];
+ }
+
+ public function key() {
+ return $this->_index[$this->_pointer];
+ }
+
+ /**
+ * 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 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);
+
+ if (!$this->_valid) {
+ $this->_valid = !is_null($this->_populate());
+ }
+
+ if ($this->_valid) {
+ $this->_pointer++;
+ }
+ return $this->_valid ? $this->current() : null;
+ }
+
+ /**
+ * Fetches all available records in the set, and returns the count.
+ *
+ * @return int Returns the number of records in the set, after all have been loaded from the
+ * resource.
+ */
+ public function count() {
+ $this->offsetGet(null);
+ return parent::count();
+ }
+
+ public function meta() {
+ return array('model' => $this->_model);
+ }
+
+ /**
+ * Converts the data in the record set to a different format, i.e. an array.
+ *
+ * @param string $format
+ * @param array $options
+ * @return mixed
+ */
+ public function to($format, $options = array()) {
+ $defaults = array('indexed' => true);
+ $options += $defaults;
+
+ $result = null;
+ $this->offsetGet(null);
+
+ switch ($format) {
+ case 'array':
+ $result = array_map(function($r) { return $r->to('array'); }, $this->_items);
+
+ if (is_scalar(current($this->_index)) && $options['indexed']) {
+ $result = array_combine($this->_index, $result);
+ }
+ break;
+ default:
+ $result = parent::to($format, $options);
+ break;
+ }
+ return $result;
+ }
+
+ public function __desctruct() {
+ $this->_close();
+ }
+
+ protected function _populate($data = null, $key = null) {
+ if ($this->_closed()) {
+ return;
+ }
+ $data = $data ?: $this->_handle->result('next', $this->_resource, $this);
+
+ if (!$data) {
+ return $this->_close();
+ }
+ $result = null;
+
+ foreach ($this->_columns as $model => $fields) {
+ $data = array_combine($fields, array_slice($data, 0, count($fields)));
+
+ $class = $this->_classes['record'];
+ $this->_items[] = ($record = new $class(compact('model', 'data')));
+ $this->_index[] = $recordKey;
+ return $record;
+ }
+ }
+
+ /**
+ * Executes when the associated result resource pointer reaches the end of its record set. The
+ * resource is freed by the connection, and the reference to the connection is unlinked.
+ *
+ * @return void
+ */
+ protected function _close() {
+ if (!$this->_closed()) {
+ $this->_resource = $this->_handle->result('close', $this->_resource, $this);
+ unset($this->_handle);
+ $this->_handle = null;
+ }
+ }
+
+ /**
+ * Checks to see if this record set has already fetched all available records and freed the
+ * associated result resource.
+ *
+ * @return boolean Returns true if all records are loaded and the database resources have been
+ * freed, otherwise returns false.
+ */
+ protected function _closed() {
+ return (empty($this->_resource) || empty($this->_handle));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/data/source/Database.php b/libraries/lithium/data/source/Database.php
new file mode 100644
index 0000000..bd6d8c3
--- /dev/null
+++ b/libraries/lithium/data/source/Database.php
@@ -0,0 +1,485 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+abstract class Database extends \lithium\data\Source {
+
+ protected $_queries = array(
+ 'select' => "
+ 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} {:constraint}"
+ );
+
+ abstract public function encoding($encoding = null);
+
+ abstract public function result($type, $resource, $context);
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'persistent' => true,
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'database' => 'lithium',
+ );
+ parent::__construct((array)$config + $defaults);
+ }
+
+ /**
+ * Checks the connection status of this database. If the `'autoConnect'` option is set to true
+ * and the database connection is not currently active, an attempt will be made to connect
+ * to the database before returning the result of the connection status.
+ *
+ * @param array $options The options available for this method:
+ * - 'autoConnect': If true, and the database connection is not currently active,
+ * calls `connect()` on this object. Defaults to `false`.
+ * @return boolean Returns the current value of `$_isConnected`, indicating whether or not
+ * the database connection is currently active. This value may not always be accurate,
+ * as the database session could have timed out or the database may have gone offline
+ * during the course of the request.
+ */
+ public function isConnected($options = array()) {
+ $defaults = array('autoConnect' => false);
+ $options += $defaults;
+
+ if (!$this->_isConnected && $options['autoConnect']) {
+ $this->connect();
+ }
+ return $this->_isConnected;
+ }
+
+ public function name($name) {
+ return $name;
+ }
+
+ public function value($value) {
+ return $value;
+ }
+
+ public function create($record, $options = array()) {
+
+ }
+
+ /**
+ * Reads records from a database using a `Query` object or raw SQL string.
+ *
+ * @param string $query
+ * @param string $options
+ * @return void
+ */
+ public function read($query, $options = array()) {
+ $defaults = array('return' => 'array');
+ $options += $defaults;
+ $params = compact('query', 'options');
+
+ return $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
+ extract($params);
+
+ if (!is_string($query)) {
+ $query = $self->renderCommand('select', $query->export($self), $query);
+ }
+ $result = $self->invokeMethod('_execute', array($query));
+
+ switch ($options['return']) {
+ case 'resource':
+ break;
+ case 'array':
+ $columns = $self->columns($query, $result);
+ $records = array();
+
+ while ($data = $self->result('next', $result, null)) {
+ $records[] = array_combine($columns, $data);
+ }
+ $self->result('close', $result, null);
+ $result = $records;
+ break;
+ }
+ return $result;
+ });
+ }
+
+ public function update($query, $options) {
+
+ }
+
+ public function delete($query, $options) {
+
+ }
+
+ public function renderCommand($type, $data, $context) {
+ if (!isset($this->_queries[$type])) {
+ throw new InvalidArgumentException("Invalid query type '{$type}'");
+ }
+ return String::insert($this->_queries[$type], $data, array('clean' => true));
+ }
+
+ public function columns($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 (empty($fields)) {
+ return array($model => array_keys($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)):
+ $scope = $field;
+ case (in_array($scope, $relations, true) && $field == '*'):
+ $scope = $ns($scope);
+ $result[$scope] = array_keys($scope::schema());
+ break;
+ case (in_array($scope, $relations)):
+ $result[$scope] = $fields;
+ break;
+ }
+ }
+ return $result;
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ function generateAssociationQuery(&$model, &$linkModel, $type, $association = null, $assocData = array(), &$queryData, $external = false, &$resultSet) {
+ if (empty($queryData['fields'])) {
+ $queryData['fields'] = $this->fields($model, $model->alias);
+ } elseif (!empty($model->hasMany) && $model->recursive > -1) {
+ $assocFields = $this->fields($model, $model->alias, array("{$model->alias}.{$model->primaryKey}"));
+ $passedFields = $this->fields($model, $model->alias, $queryData['fields']);
+
+ if (count($passedFields) === 1) {
+ $match = strpos($passedFields[0], $assocFields[0]);
+ $match1 = strpos($passedFields[0], 'COUNT(');
+ if ($match === false && $match1 === false) {
+ $queryData['fields'] = array_merge($passedFields, $assocFields);
+ } else {
+ $queryData['fields'] = $passedFields;
+ }
+ } else {
+ $queryData['fields'] = array_merge($passedFields, $assocFields);
+ }
+ unset($assocFields, $passedFields);
+ }
+
+ if ($linkModel == null) {
+ return $this->buildStatement(
+ array(
+ 'fields' => array_unique($queryData['fields']),
+ 'table' => $this->fullTableName($model),
+ 'alias' => $model->alias,
+ 'limit' => $queryData['limit'],
+ 'offset' => $queryData['offset'],
+ 'joins' => $queryData['joins'],
+ 'conditions' => $queryData['conditions'],
+ 'order' => $queryData['order'],
+ 'group' => $queryData['group']
+ ),
+ $model
+ );
+ }
+ if ($external && !empty($assocData['finderQuery'])) {
+ return $assocData['finderQuery'];
+ }
+
+ $alias = $association;
+ $self = ($model->name == $linkModel->name);
+ $fields = array();
+
+ if ((!$external && in_array($type, array('hasOne', 'belongsTo')) && $this->__bypass === false) || $external) {
+ $fields = $this->fields($linkModel, $alias, $assocData['fields']);
+ }
+ if (empty($assocData['offset']) && !empty($assocData['page'])) {
+ $assocData['offset'] = ($assocData['page'] - 1) * $assocData['limit'];
+ }
+ $assocData['limit'] = $this->limit($assocData['limit'], $assocData['offset']);
+
+ switch ($type) {
+ case 'hasOne':
+ case 'belongsTo':
+ $conditions = $this->__mergeConditions(
+ $assocData['conditions'],
+ $this->getConstraint($type, $model, $linkModel, $alias, array_merge($assocData, compact('external', 'self')))
+ );
+
+ if (!$self && $external) {
+ foreach ($conditions as $key => $condition) {
+ if (is_numeric($key) && strpos($condition, $model->alias . '.') !== false) {
+ unset($conditions[$key]);
+ }
+ }
+ }
+
+ if ($external) {
+ $query = array_merge($assocData, array(
+ 'conditions' => $conditions,
+ 'table' => $this->fullTableName($linkModel),
+ 'fields' => $fields,
+ 'alias' => $alias,
+ 'group' => null
+ ));
+ $query = array_merge(array('order' => $assocData['order'], 'limit' => $assocData['limit']), $query);
+ } else {
+ $join = array(
+ 'table' => $this->fullTableName($linkModel),
+ 'alias' => $alias,
+ 'type' => isset($assocData['type']) ? $assocData['type'] : 'LEFT',
+ 'conditions' => trim($this->conditions($conditions, true, false, $model))
+ );
+ $queryData['fields'] = array_merge($queryData['fields'], $fields);
+
+ if (!empty($assocData['order'])) {
+ $queryData['order'][] = $assocData['order'];
+ }
+ if (!in_array($join, $queryData['joins'])) {
+ $queryData['joins'][] = $join;
+ }
+ return true;
+ }
+ break;
+ case 'hasMany':
+ $assocData['fields'] = $this->fields($linkModel, $alias, $assocData['fields']);
+ if (!empty($assocData['foreignKey'])) {
+ $assocData['fields'] = array_merge($assocData['fields'], $this->fields($linkModel, $alias, array("{$alias}.{$assocData['foreignKey']}")));
+ }
+ $query = array(
+ 'conditions' => $this->__mergeConditions($this->getConstraint('hasMany', $model, $linkModel, $alias, $assocData), $assocData['conditions']),
+ 'fields' => array_unique($assocData['fields']),
+ 'table' => $this->fullTableName($linkModel),
+ 'alias' => $alias,
+ 'order' => $assocData['order'],
+ 'limit' => $assocData['limit'],
+ 'group' => null
+ );
+ break;
+ }
+ if (isset($query)) {
+ return $this->buildStatement($query, $model);
+ }
+ return null;
+ }
+
+ function queryAssociation(&$model, &$linkModel, $type, $association, $assocData, &$queryData, $external = false, &$resultSet, $recursive, $stack) {
+ if ($query = $this->generateAssociationQuery($model, $linkModel, $type, $association, $assocData, $queryData, $external, $resultSet)) {
+ if (!isset($resultSet) || !is_array($resultSet)) {
+ if (Configure::read() > 0) {
+ e('<div style = "font: Verdana bold 12px; color: #FF0000">' . sprintf(__('SQL Error in model %s:', true), $model->alias) . ' ');
+ if (isset($this->error) && $this->error != null) {
+ e($this->error);
+ }
+ e('</div>');
+ }
+ return null;
+ }
+ $count = count($resultSet);
+
+ if ($type === 'hasMany' && empty($assocData['limit']) && !empty($assocData['foreignKey'])) {
+ $ins = $fetch = array();
+ for ($i = 0; $i < $count; $i++) {
+ if ($in = $this->insertQueryData('{$__lithiumID__$}', $resultSet[$i], $association, $assocData, $model, $linkModel, $stack)) {
+ $ins[] = $in;
+ }
+ }
+
+ if (!empty($ins)) {
+ $fetch = $this->fetchAssociated($model, $query, $ins);
+ }
+
+ if (!empty($fetch) && is_array($fetch)) {
+ if ($recursive > 0) {
+ foreach ($linkModel->__associations as $type1) {
+ foreach ($linkModel->{$type1} as $assoc1 => $assocData1) {
+ $deepModel =& $linkModel->{$assoc1};
+ $tmpStack = $stack;
+ $tmpStack[] = $assoc1;
+
+ if ($linkModel->useDbConfig === $deepModel->useDbConfig) {
+ $db =& $this;
+ } else {
+ $db =& ConnectionManager::getDataSource($deepModel->useDbConfig);
+ }
+ $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack);
+ }
+ }
+ }
+ }
+ $this->__filterResults($fetch, $model);
+ return $this->__mergeHasMany($resultSet, $fetch, $association, $model, $linkModel, $recursive);
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $row =& $resultSet[$i];
+ $selfJoin = false;
+
+ if ($linkModel->name === $model->name) {
+ $selfJoin = true;
+ }
+
+ if (!empty($fetch) && is_array($fetch)) {
+ if ($recursive > 0) {
+ foreach ($linkModel->__associations as $type1) {
+ foreach ($linkModel->{$type1} as $assoc1 => $assocData1) {
+ $deepModel =& $linkModel->{$assoc1};
+
+ if (($type1 === 'belongsTo') || ($deepModel->alias === $model->alias && $type === 'belongsTo') || ($deepModel->alias != $model->alias)) {
+ $tmpStack = $stack;
+ $tmpStack[] = $assoc1;
+ if ($linkModel->useDbConfig == $deepModel->useDbConfig) {
+ $db =& $this;
+ } else {
+ $db =& ConnectionManager::getDataSource($deepModel->useDbConfig);
+ }
+ $db->queryAssociation($linkModel, $deepModel, $type1, $assoc1, $assocData1, $queryData, true, $fetch, $recursive - 1, $tmpStack);
+ }
+ }
+ }
+ }
+ $this->__mergeAssociation($resultSet[$i], $fetch, $association, $type, $selfJoin);
+ if (isset($resultSet[$i][$association])) {
+ $resultSet[$i][$association] = $linkModel->afterFind($resultSet[$i][$association]);
+ }
+ } else {
+ $tempArray[0][$association] = false;
+ $this->__mergeAssociation($resultSet[$i], $tempArray, $association, $type, $selfJoin);
+ }
+ }
+ }
+ }
+
+ public function conditions($conditions, $context) {
+ if (empty($conditions)) {
+ return '';
+ }
+ }
+
+ public function fields($fields, $context) {
+ return empty($fields) ? '*' : join(', ', $fields);
+ }
+
+ public function limit($limit, $context) {
+ if (empty($limit)) {
+ return '';
+ }
+ $result = '';
+ $offset = $context->offset() ?: '';
+
+ if (!empty($offset)) {
+ $offset .= ', ';
+ }
+ return "LIMIT {$offset}{$limit}";
+ }
+
+ function order($order, $context) {
+ if (is_string($order) && strpos($order, ',') && !preg_match('/\(.+\,.+\)/', $order)) {
+ $order = array_map('trim', explode(',', $order));
+ }
+ $order = (is_array($order) ? array_filter($order) : $order);
+
+ if (empty($order)) {
+ return '';
+ }
+
+ if (is_array($keys)) {
+ $keys = (Set::countDim($keys) > 1) ? array_map(array(&$this, 'order'), $keys) : $keys;
+
+ 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;
+ }
+
+ 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);
+ }
+ return ' ORDER BY ' . trim(str_replace('ORDER BY', '', join(',', $order)));
+ }
+ $keys = preg_replace('/ORDER\\x20BY/i', '', $keys);
+
+ if (strpos($keys, '.')) {
+ preg_match_all('/([a-zA-Z0-9_]{1,})\\.([a-zA-Z0-9_]{1,})/', $keys, $result, PREG_PATTERN_ORDER);
+ $pregCount = count($result[0]);
+
+ for ($i = 0; $i < $pregCount; $i++) {
+ if (!is_numeric($result[0][$i])) {
+ $keys = preg_replace('/' . $result[0][$i] . '/', $this->name($result[0][$i]), $keys);
+ }
+ }
+ $result = ' ORDER BY ' . $keys;
+ return $result . (!preg_match('/\\x20ASC|\\x20DESC/i', $keys) ? ' ' . $direction : '');
+
+ } elseif (preg_match('/(\\x20ASC|\\x20DESC)/i', $keys, $match)) {
+ $direction = $match[1];
+ return ' ORDER BY ' . preg_replace('/' . $match[1] . '/', '', $keys) . $direction;
+ }
+ return ' ORDER BY ' . $keys . ' ' . $direction;
+ }
+
+ /**
+ * Returns a fully-qualified table name (i.e. with prefix), quoted.
+ *
+ * @param string $entity
+ * @return string
+ */
+ protected function _entityName($entity) {
+ return $this->name($entity);
+ }
+}
+
+?>
diff --git a/libraries/lithium/data/source/Http.php b/libraries/lithium/data/source/Http.php
new file mode 100644
index 0000000..b7e8cf4
--- /dev/null
+++ b/libraries/lithium/data/source/Http.php
@@ -0,0 +1,246 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source;
+
+use lithium\core\Libraries;
+
+/**
+ * Http class to access data sources using Socket classes
+ */
+class Http extends \lithium\data\Source {
+
+ /**
+ * Fully-namespaced class references
+ *
+ * @var array
+ */
+ protected $_classes = array(
+ 'request' => '\lithium\http\Request',
+ 'response' => '\lithium\http\Response'
+ );
+
+ /**
+ * Socket connection
+ *
+ * @var object lithium\util\Socket
+ */
+ protected $_connection = null;
+
+ /**
+ * Is Connected?
+ *
+ * @var boolean
+ */
+ protected $_isConnected = false;
+
+ /**
+ * Request Object
+ *
+ * @var object
+ */
+ public $request = null;
+
+ /**
+ * Holds all parameters of the request
+ * Cast to object in the constructor
+ *
+ * @var object
+ */
+ public $response = null;
+
+ /**
+ * Constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'adapter' => 'Stream',
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'version' => '1.1',
+ 'auth' => 'Basic',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 30,
+ 'encoding' => 'UTF-8'
+ );
+ $config = (array)$config + $defaults;
+
+ $config['auth'] = array(
+ 'method' => $config['auth'],
+ 'username' => $config['login'],
+ 'password' => $config['password']
+ );
+
+ parent::__construct($config);
+ }
+
+ protected function _init() {
+ $socket = $this->_config['adapter'];
+ if (!class_exists($socket)) {
+ $socket = Libraries::locate('sockets.util', $this->_config['adapter']);
+ }
+ $this->_connection = new $socket($this->_config);
+ $this->request = new $this->_classes['request']($this->_config);
+ }
+
+ /**
+ * Connect to datasource
+ *
+ * @return boolean
+ */
+ public function connect() {
+ if (!$this->_isConnected && $this->_connection->open()) {
+ $this->_isConnected = true;
+ }
+ return $this->_isConnected;
+ }
+
+ /**
+ * Disconnect from datasource
+ *
+ * @return boolean
+ */
+ public function disconnect() {
+ if ($this->_isConnected) {
+ if ($this->_connection->close()) {
+ $this->_isConnected = false;
+ }
+ }
+ return !$this->_isConnected;
+ }
+
+ public function entities($class = null) {
+ return array();
+ }
+
+ public function describe($entity, $meta = array()) {
+ }
+
+ /**
+ * Send GET request
+ *
+ * @return string
+ */
+ public function get($path = null, $params = array()) {
+ if ($this->connect() === false) {
+ return false;
+ }
+ $this->request->method = 'GET';
+ $this->request->params = $params;
+ return $this->_send($path);
+ }
+
+ /**
+ * Send POST request
+ *
+ * @return string
+ */
+ public function post($path = null, $data = array()) {
+ if ($this->connect() === false) {
+ return false;
+ }
+ $this->request->method = 'POST';
+ if (!empty($data)) {
+ $this->request->headers(array(
+ 'Content-Type' => 'application/x-www-form-urlencoded',
+ ));
+ $this->request->body(
+ substr($this->request->queryString($data), 1)
+ );
+ }
+ return $this->_send($path);
+ }
+
+ /**
+ * Send PUT request
+ *
+ * @return string
+ */
+ public function put($path = null, $params = array()) {
+ if ($this->connect() === false) {
+ return false;
+ }
+ $this->request->method = 'PUT';
+ return $this->_send($path, $params);
+ }
+
+ /**
+ * Send DELETE request
+ *
+ * @return string
+ */
+ public function del($path = null, $params = array()) {
+ if ($this->connect() === false) {
+ return false;
+ }
+ $this->request->method = 'DELETE';
+ return $this->_send($path, $params);
+ }
+
+ /**
+ * Create used by model to POST
+ *
+ * @return string
+ */
+ public function create($record, $options = array()) {
+ return $this->post();
+ }
+
+ /**
+ * Read used by model to GET
+ *
+ * @return string
+ */
+ public function read($query = array(), $options = array()) {
+ return $this->get();
+ }
+
+ /**
+ * Update used by model to PUT
+ *
+ * @return string
+ */
+ public function update($query, $options = array()) {
+ return $this->put();
+ }
+
+ /**
+ * Used by model to DELETE
+ *
+ * @return string
+ */
+ public function delete($query, $options = array()) {
+ return $this->del();
+ }
+
+ /**
+ * Send request and return response data
+ *
+ * @return string
+ */
+ protected function _send($path = null) {
+ $this->request->path .= $path;
+
+ if ($this->_connection->write((string) $this->request)) {
+ $message = $this->_connection->read();
+ $this->response = new $this->_classes['response'](compact('message'));
+ return $this->response->body();
+ }
+ }
+}
+
+?>
\ 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
new file mode 100644
index 0000000..a0eb624
--- /dev/null
+++ b/libraries/lithium/data/source/database/adapter/MySql.php
@@ -0,0 +1,267 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\database\adapter;
+
+class MySql extends \lithium\data\source\Database {
+
+ /**
+ * MySQL column type definitions.
+ *
+ * @var array
+ */
+ protected $_columns = array(
+ 'primary_key' => array('name' => 'NOT NULL AUTO_INCREMENT'),
+ 'string' => array('name' => 'varchar', 'length' => 255),
+ 'text' => array('name' => 'text'),
+ 'integer' => array('name' => 'int', 'length' => 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'
+ ),
+ 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
+ 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
+ 'binary' => array('name' => 'blob'),
+ 'boolean' => array('name' => 'tinyint', 'length' => 1)
+ );
+
+ /**
+ * MySQL-specific value denoting whether or not table aliases should be used in DELETE and
+ * UPDATE queries.
+ *
+ * @var boolean
+ */
+ protected $_useAlias = true;
+
+ /**
+ * Constructs the MySQL adapter and sets the default port to 3306.
+ *
+ * @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:
+ * -'port': Accepts a port number or Unix socket name to use when connecting to the
+ * database. Defaults to '3306'.
+ */
+ public function __construct($config = array()) {
+ $defaults = array('port' => '3306');
+ parent::__construct((array)$config + $defaults);
+ }
+
+ /**
+ * Connects to the database using options provided to the class constructor.
+ *
+ * @return boolean True if the database could be connected, else false
+ */
+ public function connect() {
+ $config = $this->_config;
+ $this->_isConnected = false;
+ $host = $config['host'] . ':' . $config['port'];
+
+ if ($config['persistent']) {
+ $this->_connection = mysql_connect($host, $config['login'], $config['password'], true);
+ } else {
+ $this->_connection = mysql_pconnect($host, $config['login'], $config['password']);
+ }
+
+ if (mysql_select_db($config['database'], $this->_connection)) {
+ $this->_isConnected = true;
+ }
+
+ if (!empty($config['encoding'])) {
+ $this->encoding($config['encoding']);
+ }
+
+ $this->_useAlias = (bool)version_compare(
+ mysql_get_server_info($this->_connection), "4.1", ">="
+ );
+ return $this->_isConnected;
+ }
+
+ public function disconnect() {
+ if ($this->_isConnected) {
+ $this->_isConnected = !mysql_close($this->_connection);
+ return !$this->_isConnected;
+ }
+ return true;
+ }
+
+ public function entities($model = null) {
+ $config = $this->_config;
+ $method = function($self, $params, $chain) use ($config) {
+ $name = $this->name($self->config['database']);
+ return $self->query("SHOW TABLES FROM {$name};");
+ };
+ return $this->_filter(__METHOD__, compact('model'), $method);
+ }
+
+ public function describe($entity, $meta = array()) {
+ $params = compact('entity', 'meta');
+ return $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
+ extract($params);
+
+ $name = $self->invokeMethod('_entityName', array($entity));
+ $columns = $self->read("DESCRIBE {$name}", array('return' => 'array'));
+ $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['type'] = $self->invokeMethod('_column', $match['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;
+ });
+ }
+
+ public function encoding($encoding = null) {
+ $encodingMap = array('UTF-8' => 'utf8');
+
+ if (empty($encoding)) {
+ $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;
+ }
+
+ public function value($value) {
+ return "'" . mysql_real_escape_string($value, $this->_connection) . "'";
+ }
+
+ /**
+ * 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.
+ *
+ * @param mixed $query
+ * @param resource $resource
+ * @param object $context
+ * @return array
+ */
+ public function columns($query, $resource = null, $context = null) {
+ if (is_object($query)) {
+ return parent::columns($query, $resource, $context);
+ }
+
+ $result = array();
+ $count = mysql_num_fields($resource);
+
+ for ($i = 0; $i < $count; $i++) {
+ $result[] = mysql_field_name($resource, $i);
+ }
+ return $result;
+ }
+
+ protected function _execute($sql, $options = array()) {
+ $params = compact('sql', 'options');
+ $conn =& $this->_connection;
+
+ return $this->_filter(__METHOD__, $params, function($self, $params, $chain) use (&$conn) {
+ extract($params);
+ $defaults = array('buffered' => true);
+ $options += $defaults;
+ $func = ($options['buffered']) ? 'mysql_query' : 'mysql_unbuffered_query';
+ return $func($sql, $conn);
+ });
+ }
+
+ protected function _results($results) {
+ $numFields = mysql_num_fields($results);
+ $index = $j = 0;
+
+ while ($j < $numFields) {
+ $column = mysql_fetch_field($results, $j);
+
+ if (!empty($column->table)) {
+ $this->map[$index++] = array($column->table, $column->name);
+ } else {
+ $this->map[$index++] = array(0, $column->name);
+ }
+ $j++;
+ }
+ }
+
+ /**
+ * 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")
+ */
+ 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)) {
+ return $real;
+ }
+ $column = array_intersect_key($column, array('type' => null, 'length' => null));
+
+ switch (true) {
+ case in_array($column['type'], array('date', 'time', 'datetime', 'timestamp')):
+ return $column;
+ case ($column['type'] == 'tinyint' && $column['length'] == '1'):
+ case ($column['type'] == 'boolean'):
+ return array('type' => 'boolean');
+ break;
+ case (strpos($column['type'], 'int') !== false):
+ $column['type'] = 'integer';
+ break;
+ case (strpos($column['type'], 'char') !== false || $column['type'] == 'tinytext'):
+ $column['type'] = 'string';
+ break;
+ case (strpos($column['type'], 'text') !== false):
+ $column['type'] = 'text';
+ break;
+ case (strpos($column['type'], 'blob') !== false || $column['type'] == 'binary'):
+ $column['type'] = 'binary';
+ break;
+ case preg_match('/float|double|decimal/', $column['type']):
+ $column['type'] = 'float';
+ break;
+ default:
+ $column['type'] = 'text';
+ break;
+ }
+ return $column;
+ }
+}
+
+?>
diff --git a/libraries/lithium/data/source/http/adapter/Curl.php b/libraries/lithium/data/source/http/adapter/Curl.php
new file mode 100644
index 0000000..d5deaaa
--- /dev/null
+++ b/libraries/lithium/data/source/http/adapter/Curl.php
@@ -0,0 +1,29 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\http\adapter;
+
+class Curl extends \lithium\data\source\Http {
+
+ public function get($url = null, $params = array()) {
+
+ }
+
+ public function post($url = null, $params = array()) {
+
+ }
+
+ public function put($url = null, $params = array()) {
+
+ }
+
+}
\ No newline at end of file
diff --git a/libraries/lithium/data/source/http/adapter/Stream.php b/libraries/lithium/data/source/http/adapter/Stream.php
new file mode 100644
index 0000000..d931fe2
--- /dev/null
+++ b/libraries/lithium/data/source/http/adapter/Stream.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\data\source\http\adapter;
+
+class Stream extends \lithium\data\source\Http {
+
+}
\ No newline at end of file
diff --git a/libraries/lithium/g11n/Catalog.php b/libraries/lithium/g11n/Catalog.php
new file mode 100644
index 0000000..5a9dcef
--- /dev/null
+++ b/libraries/lithium/g11n/Catalog.php
@@ -0,0 +1,161 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Catalog extends \lithium\core\StaticObject {
+
+ protected static $_configurations = null;
+
+ public static function __init() {
+ static::$_configurations = new Collection();
+ }
+
+ public static function config($config = null) {
+ $default = array('adapter' => null, 'scope' => null);
+
+ if ($config) {
+ $items = array_map(function($i) use ($default) { return $i + $default; }, $config);
+ static::$_configurations = new Collection(compact('items'));
+ }
+ return static::$_configurations;
+ }
+
+ /**
+ * Reads data. Data can be obtained for one or multiple configurations
+ * and locales. The results for list-like categories are aggregated by
+ * querying all requested configurations for the requested locale and then
+ * repeating this process for all locales down the locale cascade. This allows
+ * for sparse data which is complemented by data from other sources or
+ * for more generic locales. Aggregation can be controlled by either specifying
+ * the configurations or a scope to use.
+ *
+ * Usage:
+ * {{{
+ * Catalog::read('message.page', array('zh', 'en'));
+ * Catalog::read('validation.postalCode', 'en_US');
+ * }}}
+ *
+ * @param string $category For a list of all valid categories
+ * please {@see catalog\adapters\Base::$_categories}.
+ * @param string|array $locales One or multiple locales.
+ * @param array $options Valid options are:
+ * - `'name'` One or multiple configuration names.
+ * - `'scope'` The scope to use.
+ * @return array|void If available the requested data, else `null`.
+ */
+ public static function read($category, $locales, $options = array()) {
+ $defaults = array('name' => null, 'scope' => null);
+ $options += $defaults;
+
+ $names = (array)$options['name'] ?: static::$_configurations->keys();
+ $results = null;
+
+ foreach ((array)$locales as $locale) {
+ foreach (Locale::cascade($locale) as $cascaded) {
+ foreach ($names as $name) {
+ $adapter = static::_adapter($name);
+
+ if (!$adapter->isSupported($category, __FUNCTION__)) {
+ continue;
+ }
+ if (!$result = $adapter->read($category, $cascaded, $options['scope'])) {
+ continue;
+ }
+ if (!is_array($result)) {
+ $results[$locale] = $result;
+ break 2;
+ }
+ if (!isset($results[$locale])) {
+ $results[$locale] = array();
+ }
+ $results[$locale] += $result;
+ }
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * Writes data.
+ *
+ * Usage:
+ * {{{
+ * $data = array(
+ * 'pl' => array(
+ * 'color' => 'Kolor'
+ * )
+ * 'ja' => array(
+ * 'color' => '色'
+ * ));
+ * Catalog::write('message.page', $data, array('name' => 'runtime'));
+ * }}}
+ *
+ * @param string $category For a list of all valid categories
+ * please {@see catalog\adapters\Base::$_categories}.
+ * @param array Data keyed by locale.
+ * @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, $data, $options = array()) {
+ $defaults = array('name' => null, 'scope' => null);
+ $options += $defaults;
+
+ $names = (array)$options['name'] ?: static::$_configurations->keys();
+
+ foreach ($names as $name) {
+ $adapter = static::_adapter($name);
+
+ if (!$adapter->isSupported($category, __FUNCTION__)) {
+ continue;
+ }
+ foreach ($data as $locale => $item) {
+ if (!$adapter->write($category, $locale, $options['scope'], $item)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public static function clear() {
+ static::__init();
+ }
+
+ public static function _adapter($name = null) {
+ if (empty($name)) {
+ $names = static::$_configurations->keys();
+ if (empty($names)) {
+ return;
+ }
+ $name = end($names);
+ }
+ if (!isset(static::$_configurations[$name])) {
+ return;
+ }
+ if (is_string(static::$_configurations[$name]['adapter'])) {
+ $config = static::$_configurations[$name];
+ $class = Libraries::locate('adapters.g11n.catalog', $config['adapter']);
+ $conf = array('adapter' => new $class($config)) + static::$_configurations[$name];
+ static::$_configurations[$name] = $conf;
+ }
+ return static::$_configurations[$name]['adapter'];
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/Locale.php b/libraries/lithium/g11n/Locale.php
new file mode 100644
index 0000000..d9eb4a3
--- /dev/null
+++ b/libraries/lithium/g11n/Locale.php
@@ -0,0 +1,134 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\g11n;
+
+use \BadMethodCallException;
+use \InvalidArgumentException;
+
+class Locale extends \lithium\core\StaticObject {
+
+ protected static $_tags = array(
+ 'language' => array('formatter' => 'strtolower'),
+ 'script' => array('formatter' => array('strtolower', 'ucfirst')),
+ 'territory' => array('formatter' => 'strtoupper'),
+ 'variant' => array('formatter' => 'strtoupper')
+ );
+
+ public static function __callStatic($method, $params = array()) {
+ $tags = static::invokeMethod('decompose', $params);
+
+ if (!array_key_exists($method, static::$_tags)) {
+ throw new BadMethodCallException("Invalid locale tag `{$method}`");
+ }
+ return isset($tags[$method]) ? $tags[$method] : null;
+ }
+
+ /**
+ * Composes a locale from locale tags.
+ *
+ * @param array $tags An array as obtained from {@see decompose()}.
+ * @return string|void A locale with tags separated by underscores or `null`
+ * if none of the passed tags could be used to compose a locale.
+ */
+ public static function compose($tags) {
+ $result = array();
+
+ foreach (static::$_tags as $name => $tag) {
+ if (isset($tags[$name])) {
+ $result[] = $tags[$name];
+ }
+ }
+ if ($result) {
+ return implode('_', $result);
+ }
+ }
+
+ /**
+ * Parses a locale into locale tags. A valid locale has the structure and format
+ * `language[_Script][_TERRITORY][_VARIANT]`. The language tag is an ISO 639-1 code,
+ * where not available ISO 639-3 and ISO 639-5 codes are allowed too. The territory
+ * tag is an ISO 3166-1 code.
+ *
+ * @param string $locale i.e. `'en'`, `'en_US'`or `'de_DE'`
+ * @return array Parsed language, script, territory and variant tags.
+ * @throws InvalidArgumentException
+ */
+ public static function decompose($locale) {
+ $regex = '(?P<language>[a-z]{2,3})';
+ $regex .= '(?:[_-](?P<script>[a-z]{4}))?';
+ $regex .= '(?:[_-](?P<territory>[a-z]{2}))?';
+ $regex .= '(?:[_-](?P<variant>[a-z]{5,}))?';
+
+ if (!preg_match("/^{$regex}$/i", $locale, $matches)) {
+ throw new InvalidArgumentException("Locale `{$locale}` could not be parsed");
+ }
+ return array_filter(array_intersect_key($matches, static::$_tags));
+ }
+
+ // public static function language($locale) {}
+
+ // public static function script($locale) {}
+
+ // public static function territory($locale) {}
+
+ // public static function variant($locale) {}
+
+ /**
+ * Returns a locale in it's canonical form with tags formatted properly.
+ *
+ * @param string $locale
+ * @return string
+ */
+ public static function canonicalize($locale) {
+ $tags = static::decompose($locale);
+
+ foreach ($tags as $name => &$tag) {
+ foreach ((array)static::$_tags[$name]['formatter'] as $formatter) {
+ $tag = $formatter($tag);
+ }
+ }
+ return static::compose($tags);
+ }
+
+ /**
+ * Cascades a locale.
+ *
+ * Usage:
+ * {{{
+ * Locale::cascade('en_US');
+ * // returns array('en_US', 'en', 'root')
+ *
+ * Locale::cascade('zh_Hans_HK_REVISED');
+ * // returns array('zh_Hans_HK_REVISED', 'zh_Hans_HK', 'zh_Hans', 'zh', 'root')
+ * }}}
+ *
+ * @return array Indexed array of locales (starting with the most specific one).
+ */
+ public static function cascade($locale) {
+ $locales[] = $locale;
+
+ if ($locale === 'root') {
+ return $locales;
+ }
+ $tags = static::decompose($locale);
+
+ while (count($tags) > 1) {
+ array_pop($tags);
+ $locales[] = static::compose($tags);
+ }
+ $locales[] = 'root';
+ return $locales;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/Message.php b/libraries/lithium/g11n/Message.php
new file mode 100644
index 0000000..55198e6
--- /dev/null
+++ b/libraries/lithium/g11n/Message.php
@@ -0,0 +1,103 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Message extends \lithium\core\StaticObject {
+
+ /**
+ * Returns the translation for a message ID. Translates messages according to current locale.
+ * You can use this method either for translating a single message or for messages with a plural
+ * form. The provided message will be used as a fall back if it isn't translateable. You may
+ * also use `String::insert()`-style placeholders within message strings and provide
+ * replacements as a separate option.
+ *
+ * Usage:
+ * {{{
+ * Message::translate('Mind the gap.');
+ * Message::translate('house', array(
+ * 'plural' => 'houses', 'count' => 23
+ * ));
+ * Message::translate('Your {:color} paintings are looking just great.', array(
+ * 'replacements' => array('color' => 'silver'),
+ * 'locale' => 'de'
+ * ));
+ * }}}
+ *
+ * @param string $singular Either a single or the singular form of the message.
+ * Used as the message ID.
+ * @param array $options Allowed keys are:
+ * - `'count'`: Used to determine the correct plural form.
+ * - `'plural'`: Used as a fall back if needed.
+ * - `'replacements'`: An array with replacements for placeholders.
+ * - `'locale'`: The target locale, defaults to current locale.
+ * @return string
+ */
+ public static function translate($singular, $options = array()) {
+ $defaults = array(
+ 'plural' => null,
+ 'count' => 1,
+ 'replacements' => array(),
+ // 'locale' => Environment::get('G11n.locale')
+ 'locale' => 'de',
+ 'scope' => null
+ );
+ extract($options + $defaults);
+
+ if (!$translated = static::_translated($singular, $locale, $count, $scope)) {
+ $translated = $count == 1 ? $singular : $plural;
+ }
+ return String::insert($translated, $replacements);
+ }
+
+ /**
+ * Retrieves a translated message version of a message ID.
+ *
+ * @param string $id The singular form of the message.
+ * @param string $locale The target locale.
+ * @param integer|void $count Used to determine the correct plural form (optional).
+ * @return string|void The translated message or `null` if $singular is not
+ * translateable or a plural rule couldn't be found.
+ *
+ * @todo Message pages need caching
+ */
+ protected static function _translated($id, $locale, $count = null, $scope = null) {
+ $result = Catalog::read('message.page', $locale, compact('scope'));
+
+ if (empty($result[$locale][$id]['translated'])) {
+ return null;
+ }
+ $translated = $result[$locale][$id]['translated'];
+
+ if (isset($count)) {
+ $result = Catalog::read('message.plural', $locale);
+
+ if (!isset($result[$locale])) {
+ return null;
+ }
+ $key = $result[$locale]($count);
+
+ if (isset($translated[$key])) {
+ return $translated[$key];
+ }
+ } else {
+ return array_shift($translated);
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/catalog/adapters/Base.php b/libraries/lithium/g11n/catalog/adapters/Base.php
new file mode 100644
index 0000000..7745e8c
--- /dev/null
+++ b/libraries/lithium/g11n/catalog/adapters/Base.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\g11n\catalog\adapters;
+
+use \lithium\util\Set;
+
+abstract class Base extends \lithium\core\Object {
+
+ protected $_categories = array(
+ 'inflection' => array(
+ 'plural' => array('read' => false, 'write' => false),
+ 'singular' => array('read' => false, 'write' => false),
+ 'uninflectedPlural' => array('read' => false, 'write' => false),
+ 'irregularPluar' => array('read' => false, 'write' => false),
+ 'transliteration' => array('read' => false, 'write' => false),
+ 'template' => array('read' => false, 'write' => false)
+ ),
+ 'list' => array(
+ 'language' => array('read' => false, 'write' => false),
+ 'script' => array('read' => false, 'write' => false),
+ 'territory' => array('read' => false, 'write' => false),
+ 'timezone' => array('read' => false, 'write' => false),
+ 'currency' => array('read' => false, 'write' => false),
+ 'template' => array('read' => false, 'write' => false)
+ ),
+ 'message' => array(
+ 'page' => array('read' => false, 'write' => false),
+ 'plural' => array('read' => false, 'write' => false),
+ 'direction' => array('read' => false, 'write' => false),
+ 'template' => array('read' => false, 'write' => false)
+ ),
+ 'validation' => array(
+ 'phone' => array('read' => false, 'write' => false),
+ 'postalCode' => array('read' => false, 'write' => false),
+ 'ssn' => array('read' => false, 'write' => false),
+ 'template' => array('read' => false, 'write' => false)
+ ));
+
+ protected function _init() {
+ parent::_init();
+ $properties = get_class_vars(__CLASS__);
+ $this->_categories = Set::merge($properties['_categories'], $this->_categories);
+ }
+
+ public function isSupported($category, $operation) {
+ $category = explode('.', $category, 2);
+ return $this->_categories[$category[0]][$category[1]][$operation];
+ }
+
+ /**
+ * Reads data.
+ *
+ * @param string $category For a list of all valid categories {@see $_categories}.
+ * @param string $locale A locale identifier.
+ * @param string $scope The scope for the current request.
+ * @return mixed
+ */
+ abstract public function read($category, $locale, $scope);
+
+ /**
+ * Writes data. Existing data is silently overwritten.
+ *
+ * @param string $category For a list of all valid categories {@see $_categories}.
+ * @param string $locale A locale identifier.
+ * @param string $scope The scope for the current request.
+ * @param mixed $data The data to write.
+ * @return void
+ */
+ abstract public function write($category, $locale, $scope, $data);
+
+ protected function _formatMessageItem($key, $value) {
+ if (!is_array($value) || !isset($value['translated'])) {
+ return array('singularId' => $key, 'translated' => (array)$value);
+ }
+ return $value;
+ }
+
+ /**
+ * Merges a message item into given data.
+ *
+ * @param array $data Data to merge item into.
+ * @param array $item Item to merge into $data.
+ * @return void
+ */
+ protected function _mergeMessageItem(&$data, $item) {
+ $id = $item['singularId'];
+
+ $defaults = array(
+ 'singularId' => null,
+ 'pluralId' => null,
+ 'translated' => array(),
+ 'fuzzy' => false,
+ 'comments' => array(),
+ 'occurrences' => array()
+ );
+ $item += $defaults;
+
+ if (!isset($data[$id])) {
+ $data[$id] = $item;
+ return;
+ }
+
+ if ($data[$id]['pluralId'] === null) {
+ $data[$id]['singularId'] = $item['singularId'];
+ $data[$id]['pluralId'] = $item['pluralId'];
+ $data[$id]['translated'] += $item['translated'];
+ }
+ if ($data[$id]['fuzzy'] === false) {
+ $data[$id]['fuzzy'] = $item['fuzzy'];
+ }
+ $data[$id]['comments'] = array_merge($data[$id]['comments'], $item['comments']);
+ $data[$id]['occurrences'] = array_merge($data[$id]['occurrences'], $item['occurrences']);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/catalog/adapters/Cldr.php b/libraries/lithium/g11n/catalog/adapters/Cldr.php
new file mode 100644
index 0000000..9aa1d34
--- /dev/null
+++ b/libraries/lithium/g11n/catalog/adapters/Cldr.php
@@ -0,0 +1,120 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\g11n\catalog\adapters;
+
+use \Exception;
+use \SimpleXmlElement;
+use \lithium\util\Inflector;
+use \lithium\g11n\Locale;
+
+class Cldr extends \lithium\g11n\catalog\adapters\Base {
+
+ protected $_categories = array(
+ 'validation' => array(
+ 'postalCode' => array('read' => true)
+ ),
+ 'lists' => array(
+ 'language' => array('read' => true),
+ 'script' => array('read' => true),
+ 'territory' => array('read' => true),
+ 'currency' => array('read' => true)
+ ));
+
+ public function __construct($config = array()) {
+ $defaults = array('path' => null, 'scope' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ if (!is_dir($this->_config['path'])) {
+ throw new Exception("Cldr directory does not exist at `{$this->_config['path']}`");
+ }
+ }
+
+ public function read($category, $locale, $scope) {
+ if ($scope !== $this->_config['scope']) {
+ return null;
+ }
+ $path = $this->_config['path'];
+ $file = $query = $yield = $post = null;
+
+ switch ($category) {
+ case 'validation.postalCode':
+ if (!$territory = Locale::territory($locale)) {
+ return null;
+ }
+ $file = "{$path}/supplemental/postalCodeData.xml";
+ $query = "/supplementalData/postalCodeData";
+ $query .= "/postCodeRegex[@territoryId=\"{$territory}\"]";
+
+ $yield = function($nodes) {
+ return (string)current($nodes);
+ };
+ $post = function($data) {
+ return "/^{$data}$/";
+ };
+ break;
+ case 'list.language':
+ case 'list.script':
+ case 'list.territory':
+ list(, $singular) = explode('.', $category, 2);
+ $plural = Inflector::pluralize($singular);
+
+ $file = "{$path}/main/{$locale}.xml";
+ $query = "/ldml/localeDisplayNames/{$plural}/{$singular}";
+
+ $yield = function($nodes) {
+ $data = null;
+
+ foreach ($nodes as $node) {
+ $data[(string)$node['type']] = (string)$node;
+ }
+ return $data;
+ };
+ break;
+ case 'list.currency':
+ $file = "{$path}/main/{$locale}.xml";
+ $query = "/ldml/numbers/currencies/currency";
+
+ $yield = function($nodes) {
+ $data = null;
+
+ foreach ($nodes as $node) {
+ $displayNames = $node->xpath('displayName');
+ $data[(string)$node['type']] = (string)current($displayNames);
+ }
+ return $data;
+ };
+ break;
+ }
+ return $this->_parseXml($file, $query, $yield, $post);
+ }
+
+ protected function _parseXml($file, $query, $yield, $post) {
+ $document = new SimpleXmlElement($file, LIBXML_COMPACT, true);
+ $nodes = $document->xpath($query);
+
+ if (!$data = $yield($nodes)) {
+ return null;
+ }
+ if ($post) {
+ return $post($data);
+ }
+ return $data;
+ }
+
+ public function write($category, $locale, $scope, $data) {}
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/catalog/adapters/Code.php b/libraries/lithium/g11n/catalog/adapters/Code.php
new file mode 100644
index 0000000..732e7bc
--- /dev/null
+++ b/libraries/lithium/g11n/catalog/adapters/Code.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\g11n\catalog\adapters;
+
+use \Exception;
+use \RecursiveIteratorIterator;
+use \RecursiveDirectoryIterator;
+
+class Code extends \lithium\g11n\catalog\adapters\Base {
+
+ protected $_categories = array(
+ 'message' => array(
+ 'template' => array('read' => true)
+ ));
+
+ public function __construct($config = array()) {
+ $defaults = array('path' => null, 'scope' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ if (!is_dir($this->_config['path'])) {
+ throw new Exception("Code directory does not exist at `{$this->_config['path']}`");
+ }
+ }
+
+ public function read($category, $locale, $scope) {
+ if ($scope !== $this->_config['scope']) {
+ return null;
+ }
+ $path = $this->_config['path'];
+
+ $base = new RecursiveDirectoryIterator($path);
+ $iterator = new RecursiveIteratorIterator($base);
+ $data = array();
+
+ foreach ($iterator as $item) {
+ $file = $item->getPathname();
+
+ switch (pathinfo($file, PATHINFO_EXTENSION)) {
+ case 'php':
+ $data += $this->_parsePhp($file);
+ break;
+ }
+ }
+ if ($data) {
+ return $data;
+ }
+ }
+
+ public function write($category, $locale, $scope, $data) {}
+
+ /**
+ * Parses a PHP file for translateable strings wrapped in `$t()` calls.
+ *
+ * @param string $file Absolute path to a PHP file.
+ * @return array
+ * @todo How should invalid entries be handled?
+ */
+ protected function _parsePhp($file) {
+ $contents = file_get_contents($file);
+
+ if (strpos($contents, '$t(') === false) {
+ return array();
+ }
+
+ $defaults = array(
+ 'singularId' => null,
+ 'pluralId' => null,
+ 'open' => false,
+ 'concat' => false,
+ 'occurrence' => array('file' => $file, 'line' => null)
+ );
+ extract($defaults);
+ $data = array();
+
+ $tokens = token_get_all($contents);
+ unset($contents);
+
+ foreach ($tokens as $key => $token) {
+ if (!is_array($token)) {
+ $token = array(0 => null, 1 => $token, 2 => null);
+ }
+
+ if (!$open) {
+ if ($token[1] === '$t' && isset($tokens[$key + 1]) && $tokens[$key + 1] === '(') {
+ $open = true;
+ $occurrence['line'] = $token[2];
+ }
+ } else {
+ if ($token[1] === '.') {
+ $concat = true;
+ } elseif ($token[1] === ',') {
+ $concat = false;
+ } elseif ($token[0] === T_CONSTANT_ENCAPSED_STRING && !isset($pluralId)) {
+ $type = isset($singularId) ? 'pluralId' : 'singularId';
+ $$type = ($concat ? $$type : null) . $this->_formatMessage($token[1]);
+ } elseif ($token[0] !== T_WHITESPACE && $token[1] !== '(') {
+ if (isset($singularId)) {
+ $this->_mergeMessageItem($data, array(
+ 'singularId' => $singularId,
+ 'pluralId' => $pluralId,
+ 'occurrences' => array($occurrence),
+ ));
+ }
+ extract($defaults, EXTR_OVERWRITE);
+ }
+ }
+ }
+ return $data;
+ }
+
+ /**
+ * Formats a string to be added as a message.
+ *
+ * @param string $string
+ * @return string
+ */
+ function _formatMessage($string) {
+ $quote = substr($string, 0, 1);
+ $string = substr($string, 1, -1);
+
+ if ($quote === '"') {
+ $string = stripcslashes($string);
+ } else {
+ $string = strtr($string, array("\\'" => "'", "\\\\" => "\\"));
+ }
+ $string = str_replace("\r\n", "\n", $string);
+ return addcslashes($string, "\0..\37\\\"");
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/catalog/adapters/Gettext.php b/libraries/lithium/g11n/catalog/adapters/Gettext.php
new file mode 100644
index 0000000..00d591c
--- /dev/null
+++ b/libraries/lithium/g11n/catalog/adapters/Gettext.php
@@ -0,0 +1,355 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\g11n\catalog\adapters;
+
+use \Exception;
+use \lithium\util\String;
+
+class Gettext extends \lithium\g11n\catalog\adapters\Base {
+
+ protected $_categories = array(
+ 'message' => array(
+ 'page' => array('read' => true, 'write' => true),
+ 'template' => array('read' => true, 'write' => true)
+ ));
+
+ public function __construct($config = array()) {
+ $defaults = array('path' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ if (!is_dir($this->_config['path'])) {
+ throw new Exception("Gettext directory does not exist at `{$this->_config['path']}`");
+ }
+ }
+
+ public function read($category, $locale, $scope) {
+ $files = $this->_files($category, $locale, $scope);
+
+ foreach ($files as $file) {
+ $method = '_parse' . ucfirst(pathinfo($file, PATHINFO_EXTENSION));
+
+ if (!$stream = fopen($file, 'rb')) {
+ continue;
+ }
+ $data = $this->invokeMethod($method, array($stream));
+ fclose($stream);
+
+ if ($data) {
+ return $data;
+ }
+ }
+ }
+
+ /**
+ * @todo readd meta data support
+ */
+ public function write($category, $locale, $scope, $data) {
+ $files = $this->_files($category, $locale, $scope);
+
+ foreach ($files as $file) {
+ $method = '_compile' . ucfirst(pathinfo($file, PATHINFO_EXTENSION));
+
+ if (!$stream = fopen($file, 'wb')) {
+ return false;
+ }
+ $this->invokeMethod($method, array($stream, $data, array()));
+ fclose($stream);
+ }
+ return true;
+ }
+
+ /**
+ * Returns absolute paths to files according to configuration.
+ *
+ * @param string $category
+ * @param string|void $locale
+ * @return array
+ */
+ protected function _files($category, $locale, $scope) {
+ $path = $this->_config['path'];
+ $files = array();
+ $scope = $scope ?: 'default';
+
+ switch ($category) {
+ case 'message.page':
+ $files[] = "{$path}/{$locale}/LC_MESSAGES/{$scope}.mo";
+ $files[] = "{$path}/{$locale}/LC_MESSAGES/{$scope}.po";
+ break;
+ case 'message.template':
+ $files[] = "{$path}/message_{$scope}.pot";
+ break;
+ }
+ return $files;
+ }
+
+ /**
+ * Parses portable object (PO) format.
+ *
+ * @param resource $stream
+ * @return array
+ */
+ protected function _parsePo($stream) {
+ $defaults = array(
+ 'singularId' => null,
+ 'pluralId' => null,
+ 'translated' => array(),
+ 'fuzzy' => false,
+ 'comments' => array(),
+ 'occurrences' => array()
+ );
+ extract($defaults);
+ $data = array();
+
+ while ($line = fgets($stream)) {
+ $line = trim($line);
+
+ if ($line === '') {
+ continue;
+ }
+
+ if (preg_match('/^#\,\sfuzzy$/', $line)) {
+ $fuzzy = true;
+ } elseif (preg_match('/^#\.\s(.+)$/', $line, $matches)) {
+ $comments[] = $matches[1];
+ } elseif (preg_match('/^#\:\s(.+):([0-9]+)$/', $line, $matches)) {
+ $occurrences[] = array('file' => $matches[1], 'line' => $matches[2]);
+ } elseif (preg_match('/^msgid\s"(.+)"$/', $line, $matches)) {
+ if ($singularId) {
+ $this->_mergeMessageItem($data, compact(
+ 'singularId', 'pluralId', 'translated',
+ 'fuzzy', 'occurrences', 'comments'
+ ));
+ extract($defaults, EXTR_OVERWRITE);
+ }
+ $singularId = stripcslashes($matches[1]);
+ } elseif (preg_match('/^msgid_plural\s"(.+)"$/', $line, $matches)) {
+ $pluralId = stripcslashes($matches[1]);
+ } elseif (preg_match('/^msgstr\s"(.+)"$/', $line, $matches)) {
+ $translated[0] = stripcslashes($matches[1]);
+ } elseif (preg_match('/^msgstr\[(\d+)\]\s"(.+)"$/', $line, $matches)) {
+ $translated[$matches[1]] = stripcslashes($matches[2]);
+ } elseif ($translated && preg_match('/^"(.+)"$/', $line, $matches)) {
+ $translated[key($translated)] .= stripcslashes($matches[1]);
+ }
+ }
+ $this->_mergeMessageItem($data, compact(
+ 'singularId', 'pluralId', 'translated',
+ 'fuzzy', 'occurrences', 'comments'
+ ));
+ return $data;
+ }
+
+ /**
+ * Parses portable object template (POT) format.
+ *
+ * @param resource $stream
+ * @return array
+ */
+ protected function _parsePot($stream) {
+ return $this->_parsePo($stream);
+ }
+
+ /**
+ * Parses machine object (MO) format.
+ *
+ * @param resource $stream
+ * @return array
+ * @throws Exception If stream content has an invalid format.
+ */
+ protected function _parseMo($stream) {
+ $magic = unpack('V1', fread($stream, 4));
+ $magic = substr(dechex(current($magic)), -8);
+
+ if ($magic == '950412de') {
+ $isBigEndian = false;
+ } elseif ($magic == 'de120495') {
+ $isBigEndian = true;
+ } else {
+ throw new Exception("MO stream content has an invalid format");
+ }
+
+ $header = array(
+ 'formatRevision' => null,
+ 'count' => null,
+ 'offsetId' => null,
+ 'offsetTranslated' => null,
+ 'sizeHashes' => null,
+ 'offsetHashes' => null
+ );
+ foreach ($header as &$value) {
+ $value = $this->_readLong($stream, $isBigEndian);
+ }
+ extract($header);
+ $data = array();
+
+ for ($i = 0; $i < $count; $i++) {
+ $singularId = $pluralId = null;
+ $translated = array();
+
+ fseek($stream, $offsetId + $i * 8);
+
+ $length = $this->_readLong($stream, $isBigEndian);
+ $offset = $this->_readLong($stream, $isBigEndian);
+
+ if ($length < 1) {
+ continue;
+ }
+
+ fseek($stream, $offset);
+ $singularId = fread($stream, $length);
+
+ if (strpos($singularId, "\000") !== false) {
+ list($singularId, $pluralId) = explode("\000", $singularId);
+ }
+
+ fseek($stream, $offsetTranslated + $i * 8);
+ $length = $this->_readLong($stream, $isBigEndian);
+ $offset = $this->_readLong($stream, $isBigEndian);
+
+ fseek($stream, $offset);
+ $translated = fread($stream, $length);
+
+ if (strpos($translated, "\000") !== false) {
+ $translated = explode("\000", $translated);
+ } else {
+ $translated = array($translated);
+ }
+
+ $this->_mergeMessageItem($data, compact(
+ 'singularId', 'pluralId', 'translated'
+ ));
+ }
+ return $data;
+ }
+
+ /**
+ * Reads an unsigned long from stream respecting endianess.
+ *
+ * @param resource $stream
+ * @param boolean $isBigEndian
+ * @return integer
+ */
+ protected function _readLong($stream, $isBigEndian) {
+ $result = unpack($isBigEndian ? 'N1' : 'V1', fread($stream, 4));
+ $result = current($result);
+ return (integer)substr($result, -8);
+ }
+
+ /**
+ * Compiles data into portable object (PO) format.
+ *
+ * @param resource $stream
+ * @param array $data
+ * @param array $meta
+ * @return boolean
+ */
+ protected function _compilePo($stream, $data, $meta) {
+ $defaults = array(
+ 'locale' => 'LOCALE',
+ 'package' => 'NAME',
+ 'packageVersion' => 'VERSION',
+ 'copyright' => 'NAME',
+ 'copyrightYear' => 'YEAR',
+ 'copyrightEmail' => 'EMAIL',
+ 'templateCreationDate' => 'DATE',
+ 'revisionDate' => 'DATE',
+ 'lastTranslator' => 'NAME',
+ 'lastTranslatorEmail' => 'EMAIL',
+ 'reportBugsTo' => 'EMAIL',
+ 'languageTeamEmail' => 'EMAIL',
+ 'pluralFormNumber' => 'NUMBER',
+ 'pluralFormRule' => 'EXPRESSION',
+ 'mimeVersion' => '1.0',
+ 'contentType' => 'text/plain',
+ 'contentTypeCharset' => 'UTF-8',
+ 'contentTypeEncoding' => '8bit',
+ );
+ $meta += $defaults;
+
+ $output = array();
+ $output[] = '# {:locale} translation of {:package} messages.';
+ $output[] = '# Copyright {:copyrightYear} {:copyright} <{:copyrightEmail}>';
+ $output[] = '# This file is distributed under the same license as the {:package} package.';
+ $output[] = '#';
+ $output[] = '"Project-Id-Version: {:package} {:packageVersion}\n"';
+ $output[] = '"POT-Creation-Date: {:templateCreationDate}\n"';
+ $output[] = '"PO-Revision-Date: {:revisionDate}\n"';
+ $output[] = '"Last-Translator: {:lastTranslator} <{:lastTranslatorEmail}>\n"';
+ $output[] = '"Language-Team: {:locale} <{:languageTeamEmail}>\n"';
+ $output[] = '"MIME-Version: {:mimeVersion}\n"';
+ $output[] = '"Content-Type: {:contentType}; charset={:contentTypeCharset}\n"';
+ $output[] = '"Content-Transfer-Encoding: {:contentTypeEncoding}\n"';
+ $output[] = '"Plural-Forms: nplurals={:pluralFormNumber}; plural={:pluralFormRule};\n"';
+ $output[] = '';
+ $output = String::insert(implode("\n", $output) . "\n", $meta);
+ fwrite($stream, $output);
+
+ foreach ($data as $key => $item) {
+ $output = array();
+ $item = $this->_formatMessageItem($key, $item);
+
+ foreach ($item['occurrences'] as $occurrence) {
+ $output[] = '#: ' . $occurrence['file'] . ':' . $occurrence['line'];
+ }
+ foreach ($item['comments'] as $comment) {
+ $output[] = '#. ' . $comment;
+ }
+ if ($item['fuzzy']) {
+ $output[] = '#, fuzzy';
+ }
+
+ $output[] = 'msgid "' . $item['singularId'] . '"';
+
+ if (!isset($item['pluralId'])) {
+ $output[] = 'msgstr "' . $item['translated'] . '"';
+ } else {
+ $output[] = 'msgid_plural "' . $item['pluralId'] . '"';
+
+ foreach ($item['translated'] as $key => $value) {
+ $output[] = 'msgstr[' . $key . '] "' . $value . '"';
+ }
+ }
+ $output[] = '';
+ $output = implode("\n", $output) . "\n";
+ fwrite($stream, $output);
+ }
+ return true;
+ }
+
+ /**
+ * Compiles data into portable object template (POT) format.
+ *
+ * @param resource $stream
+ * @param array $data
+ * @param array $meta
+ * @return boolean Success.
+ */
+ protected function _compilePot($stream, $data, $meta) {
+ return $this->_compilePo($stream, $data, $meta);
+ }
+
+ /**
+ * Compiles data into machine object (MO) format.
+ *
+ * @param resource $stream
+ * @param array $data
+ * @param array $meta
+ * @return void
+ */
+ protected function _compileMo($stream, $data, $meta) {}
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/g11n/catalog/adapters/Memory.php b/libraries/lithium/g11n/catalog/adapters/Memory.php
new file mode 100644
index 0000000..fe9c45a
--- /dev/null
+++ b/libraries/lithium/g11n/catalog/adapters/Memory.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\g11n\catalog\adapters;
+
+class Memory extends \lithium\g11n\catalog\adapters\Base {
+
+ protected $_categories = array(
+ 'inflection' => array(
+ 'plural' => array('read' => true, 'write' => true),
+ 'singular' => array('read' => true, 'write' => true),
+ 'uninflectedPlural' => array('read' => true, 'write' => true),
+ 'irregularPluar' => array('read' => true, 'write' => true),
+ 'transliteration' => array('read' => true, 'write' => true),
+ 'template' => array('read' => true, 'write' => true)
+ ),
+ 'list' => array(
+ 'language' => array('read' => true, 'write' => true),
+ 'script' => array('read' => true, 'write' => true),
+ 'territory' => array('read' => true, 'write' => true),
+ 'timezone' => array('read' => true, 'write' => true),
+ 'currency' => array('read' => true, 'write' => true),
+ 'template' => array('read' => true, 'write' => true)
+ ),
+ 'message' => array(
+ 'page' => array('read' => true, 'write' => true),
+ 'plural' => array('read' => true, 'write' => true),
+ 'direction' => array('read' => true, 'write' => true),
+ 'template' => array('read' => true, 'write' => true)
+ ),
+ 'validation' => array(
+ 'phone' => array('read' => true, 'write' => true),
+ 'postalCode' => array('read' => true, 'write' => true),
+ 'ssn' => array('read' => true, 'write' => true),
+ 'template' => array('read' => true, 'write' => true)
+ ));
+
+ protected $_data = array();
+
+ public function read($category, $locale, $scope) {
+ if (isset($this->_data[$scope][$category][$locale])) {
+ return $this->_data[$scope][$category][$locale];
+ }
+ }
+
+ public function write($category, $locale, $scope, $data) {
+ switch ($category) {
+ case 'message.page':
+ case 'message.template':
+ foreach ($data as $key => $item) {
+ $item = $this->_formatMessageItem($key, $item);
+ $this->_mergeMessageItem($this->_data[$scope][$category][$locale], $item);
+ }
+ break;
+ default:
+ if (is_array($data)) {
+ if (!isset($this->_data[$scope][$category][$locale])) {
+ $this->_data[$scope][$category][$locale] = array();
+ }
+ $this->_data[$scope][$category][$locale] += $data;
+ } else {
+ $this->_data[$scope][$category][$locale] = $data;
+ }
+ break;
+ }
+ return true;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/http/Base.php b/libraries/lithium/http/Base.php
new file mode 100644
index 0000000..10aedb3
--- /dev/null
+++ b/libraries/lithium/http/Base.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+/**
+* Base class for Http Request and Response
+*
+*/
+class Base {
+ /**
+ * 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
+ *
+ * @return array
+ */
+ public function body($data = null) {
+ $this->body = array_merge((array)$this->body, (array)$data);
+ return join("\r\n", $this->body);
+ }
+
+}
\ No newline at end of file
diff --git a/libraries/lithium/http/Media.php b/libraries/lithium/http/Media.php
new file mode 100644
index 0000000..eaff58e
--- /dev/null
+++ b/libraries/lithium/http/Media.php
@@ -0,0 +1,270 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+
+use \Exception;
+use \lithium\util\String;
+
+class Media extends \lithium\core\Object {
+
+ /**
+ * Maps file extensions to content-types. Used to set response types and determine request
+ * types. Can be modified with Media::type().
+ *
+ * @var array
+ * @see lithium\http\Media::type()
+ */
+ protected static $_types = array(
+ 'ai' => 'application/postscript',
+ 'amf' => 'application/x-amf',
+ 'atom' => 'application/atom+xml',
+ 'bin' => 'application/octet-stream',
+ 'bz2' => 'application/x-bzip',
+ 'class' => 'application/octet-stream',
+ 'css' => 'text/css',
+ 'csv' => array('application/csv', 'application/vnd.ms-excel'),
+ 'file' => 'multipart/form-data',
+ 'form' => 'application/x-www-form-urlencoded',
+ 'htm' => array('alias' => 'html'),
+ 'html' => array('text/html', '*/*'),
+ 'js' => 'text/javascript',
+ 'json' => 'application/json',
+ 'pdf' => 'application/pdf',
+ 'rss' => 'application/rss+xml',
+ 'swf' => 'application/x-shockwave-flash',
+ 'tar' => 'application/x-tar',
+ 'text' => 'text/plain',
+ 'txt' => array('alias' => 'text'),
+ 'vcf' => 'text/x-vcard',
+ 'xhtml' => array('application/xhtml+xml', 'application/xhtml', 'text/xhtml'),
+ 'xhtml-mobile' => 'application/vnd.wap.xhtml+xml',
+ 'xml' => array('application/xml', 'text/xml'),
+ 'zip' => 'application/x-zip',
+ );
+
+ /**
+ * A map of media handler objects or callbacks, mapped to media types.
+ *
+ * @var array
+ */
+ protected static $_handlers = array(
+ 'default' => array(
+ 'view' => '\lithium\template\View',
+ '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' => null
+ );
+
+ 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')
+ ))
+ );
+
+ /**
+ * Returns the list of registered media types. New types can be set with the `type()` method.
+ *
+ * @return array Returns an array of media type extensions or short-names, which comprise the
+ * list of types handled.
+ */
+ public static function types() {
+ return array_keys(static::$_types);
+ }
+
+ /**
+ * Map an extension to a particular content-type (or types) with a set of options.
+ *
+ * @param string $type A file extension for the type, i.e. 'txt', 'js', or 'atom'
+ * @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.
+ * @param array $options Optional. The handling options for this media type.
+ * @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\http\Media::$_types
+ * @see lithium\http\Media::$_handlers
+ */
+ public static function type($type, $content = null, $options = array()) {
+ if (empty($content) && empty($options)) {
+ return array(
+ 'content' => (isset(static::$_types[$type]) ? static::$_types[$type] : null),
+ 'options' => (isset(static::$_handlers[$type]) ? static::$_handlers[$type] : null)
+ );
+ }
+ if (!empty($content)) {
+ static::$_types[$type] = $content;
+ }
+ if (!empty($options)) {
+ static::$_handlers[$type] = $options;
+ }
+ }
+
+ /**
+ * Gets or sets options for various asset types.
+ *
+ * @param string $type
+ * @param string $options
+ * @return void
+ */
+ public static function assets($type = null, $options = array()) {
+ if (empty($type)) {
+ return static::$_assets;
+ }
+ if (empty($options)) {
+ return isset(static::$_assets[$type]) ? static::$_assets[$type] : null;
+ }
+
+ if (isset(static::$_assets[$type])) {
+ static::$_assets[$type] = $options + static::$_assets[$type];
+ } else {
+ static::$_assets[$type] = $options;
+ }
+ }
+
+ /**
+ * Calculates the web-accessible path to a static asset, usually a JavaScript, CSS or image
+ * file.
+ *
+ * @param string $path
+ * @param string $type
+ * @param array $options
+ * @return string
+ */
+ public static function asset($path, $type, $options = array()) {
+ if (preg_match('/^[a-z0-9-]+:\/\//i', $path)) {
+ return $path;
+ }
+ $type = isset(static::$_assets[$type]) ? $type : 'generic';
+
+ $defaults = array(
+ 'base' => null, 'timestamp' => false, 'filter' => null,
+ 'path' => array(), 'suffix' => null
+ );
+ $options += (static::$_assets[$type] + $defaults);
+
+ if ($path[0] !== '/') {
+ end($options['path']);
+ $path = String::insert(rtrim(key($options['path']), '/'), compact('path') + $options);
+ }
+
+ if (strpos($path, '?') === false) {
+ if ($options['suffix'] && strpos($path, $options['suffix']) === false) {
+ $path .= $options['suffix'];
+ }
+ if ($options['timestamp']) {
+ $path .= '?' . @filemtime(WWW_ROOT . $path);
+ }
+ }
+
+ if (is_array($options['filter']) && $options['filter']) {
+ $path = str_replace($options['filter'][0], $options['filter'][1], $path);
+ }
+ return $path;
+ }
+
+ /**
+ * Renders data (usually the result of a controller action) and generates a string
+ * representation of it, based on the type of expected output.
+ *
+ * @param object $response A reference to a Response object into which the operation will be
+ * rendered. The content of the render operation will be assigned to the `$body`
+ * property of the object, and the `'Content-type'` header will be set
+ * accordingly.
+ * @param mixed $data
+ * @param array $options
+ * @return void
+ * @todo Implement proper exception handling
+ */
+ public static function render(&$response, $data = null, $options = array()) {
+ $defaults = array(
+ 'encode' => function($data) { return print_r($data, true); },
+ 'template' => null,
+ 'layout' => null,
+ 'view' => null
+ );
+ $options += array('type' => $response->type());
+ $type = $options['type'];
+ $result = null;
+
+ if (!array_key_exists($type, static::$_types)) {
+ throw new Exception("Unhandled type '$type'");
+ }
+
+ $h = array_key_exists($type, static::$_handlers) ? static::$_handlers[$type] : null;
+ $h = is_null($h) ? $defaults : $h + static::$_handlers['default'] + $defaults;
+
+ $response->body(static::_handle($h, $data, $options));
+ $response->headers('Content-type', current((array)static::$_types[$type]));
+ }
+
+ /**
+ * 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.
+ *
+ * @param array $handler
+ * @param array $data
+ * @param array $options
+ * @return string
+ */
+ protected static function _handle($handler, $data, $options) {
+ $result = '';
+
+ if (isset($options['request'])) {
+ $options += (array)$options['request']->params;
+ $handler['request'] = $options['request'];
+ }
+
+ switch (true) {
+ case $handler['view']:
+ $view = new $handler['view']($handler);
+ $result = $view->render('all', $data, $options);
+ break;
+ case $handler['encode']:
+ $method = $handler['encode'];
+ $result = is_string($method) ? $method($data) : $method($data, $handler);
+ break;
+ default:
+
+ break;
+ }
+ return $result;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/http/Request.php b/libraries/lithium/http/Request.php
new file mode 100644
index 0000000..dcd0939
--- /dev/null
+++ b/libraries/lithium/http/Request.php
@@ -0,0 +1,170 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+
+use \lithium\util\String;
+
+class Request extends \lithium\http\Base {
+
+ /**
+ * The Host header value and authority
+ *
+ * @var string
+ **/
+ public $host = 'localhost';
+
+ /**
+ * Port number
+ *
+ * @var string
+ **/
+ public $port = 80;
+
+ /**
+ * the method of the request
+ * GET, POST, PUT, DELETE, OPTIONS, HEAD, TRACE, CONNECT
+ *
+ * @var string
+ **/
+ public $method = 'GET';
+
+ /**
+ * absolute path of the request
+ *
+ * @var string
+ **/
+ public $path = '/';
+
+ /**
+ * Used to build query string
+ *
+ * @var array
+ */
+ public $params = array();
+
+ /**
+ * headers
+ *
+ * @var array
+ */
+ public $headers = array(
+ 'Host' => 'localhost:80',
+ 'Connection' => 'Close',
+ 'User-Agent' => 'Mozilla/5.0 (Lithium)'
+ );
+
+ /**
+ * The authentication/authorization information
+ *
+ * @var array
+ */
+ public $auth = array(
+ /* 'method' => 'Basic',
+ 'username' => null,
+ 'password' => null,
+ */
+ );
+
+ /**
+ * cookies
+ *
+ * @var array
+ */
+ public $cookies = array();
+
+ /**
+ * body
+ *
+ * @var array
+ */
+ public $body = array();
+
+ /**
+ * Constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ foreach ($config as $key => $value) {
+ if (isset($this->{$key}) && !is_array($this->{$key})) {
+ $this->{$key} = $value;
+ }
+ }
+ $this->protocol = "HTTP/{$this->version}";
+
+ $this->headers('Host', $this->host . ":" . $this->port);
+
+ if (!empty($config['headers'])) {
+ $this->headers($config['headers']);
+ }
+ if (!empty($config['body'])) {
+ $this->body($config['body']);
+ }
+ if (!empty($config['params'])) {
+ $this->params = $config['params'];
+ }
+ if (!empty($config['auth'])) {
+ $this->headers('Authorization',
+ $config['auth']['method'] . ' '
+ . base64_encode(
+ $config['auth']['username'] . ':'
+ . $config['auth']['password']
+ )
+ );
+ }
+ }
+
+ /**
+ * Set queryString
+ *
+ * @return array
+ */
+ public function queryString($params = array(), $format = "{:key}={:value}&") {
+ $query = null;
+ if (is_string($params)) {
+ list($format, $params) = func_get_args();
+ }
+ $params = array_merge((array)$this->params, $params);
+ foreach ((array) $params as $key => $value) {
+ $query .= String::insert($format, array(
+ 'key' => urlencode($key), 'value' => urlencode($value)
+ ));
+ }
+ if (empty($query)) {
+ return null;
+ }
+ return "?" . substr($query, 0, -1);
+ }
+
+ /**
+ * magic method to convert object to string
+ *
+ * @return string
+ */
+ public function __toString() {
+ $query = $this->queryString();
+ $path = str_replace('//', '/', $this->path) . $query;
+
+ $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);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/http/Response.php b/libraries/lithium/http/Response.php
new file mode 100644
index 0000000..7932be5
--- /dev/null
+++ b/libraries/lithium/http/Response.php
@@ -0,0 +1,201 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+
+class Response extends \lithium\http\Base {
+
+ /**
+ * Status Code and Message
+ *
+ * @var array
+ **/
+ public $status = array('code' => 200, 'message' => 'OK');
+
+ /**
+ * headers
+ *
+ * @var array
+ **/
+ public $headers = array();
+
+ /**
+ * Content Type
+ *
+ * @var string
+ **/
+ public $type = 'text/html';
+
+ /**
+ * Character Set
+ *
+ * @var string
+ **/
+ public $charset = 'UTF-8';
+
+ /**
+ * the body
+ *
+ * @var array
+ **/
+ public $body = array();
+
+ /**
+ * Status codes
+ *
+ * @var array
+ */
+ protected $_statuses = array(
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Time-out',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Requested range not satisfiable',
+ 417 => 'Expectation Failed',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Time-out'
+ );
+
+ /**
+ * Constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ foreach ($config as $key => $value) {
+ if (isset($this->{$key})) {
+ $this->{$key} = $value;
+ }
+ }
+ if (!empty($config['message'])) {
+ $parts = explode("\r\n\r\n", $config['message'], 2);
+
+ if (empty($parts)) {
+ return false;
+ }
+ $headers = str_replace("\r", "", explode("\n", array_shift($parts)));
+
+ if (empty($headers)) {
+ return false;
+ }
+ preg_match('/HTTP\/(\d+\.\d+)\s+(\d+)\s+(\w+)/i',
+ array_shift($headers), $match
+ );
+ if (!empty($match)) {
+ list($line, $this->version,
+ $this->status['code'], $this->status['message']
+ ) = $match;
+ }
+ $this->protocol = "HTTP/{$this->version}";
+
+ $this->headers($headers);
+
+ if (!empty($this->headers['Content-Type'])) {
+ preg_match('/^(.*?);charset=(.+)/i',
+ $this->headers['Content-Type'], $match
+ );
+ if (!empty($match)) {
+ $this->type = trim($match[1]);
+ $this->charset = trim($match[2]);
+ }
+ }
+
+ $this->body(array_shift($parts));
+ }
+ }
+
+ /**
+ * undocumented function
+ *
+ * @return string
+ *
+ **/
+ public function status($key = null, $data = null) {
+ if ($data === null) {
+ $data = $key;
+ }
+ if (!empty($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 {
+ $statuses = array_flip($this->_statuses);
+ if (isset($statuses[$data])) {
+ $this->status = array(
+ 'code' => $statuses[$data], 'message' => $data
+ );
+ }
+ }
+ }
+ if (!isset($this->_statuses[$this->status['code']])) {
+ return false;
+ }
+ if (isset($this->status[$key])) {
+ return $this->status[$key];
+ }
+ return "{$this->protocol}"
+ . " {$this->status['code']} {$this->status['message']}";
+ }
+
+ /**
+ * Return the response as a string
+ *
+ * @return string
+ */
+ public function __toString() {
+ $first = "{$this->protocol}"
+ . " {$this->status['code']} {$this->status['message']}";
+
+ $response = array(
+ $first, join("\r\n", $this->headers()),
+ "", $this->body()
+ );
+
+ $message = join("\r\n", $response);
+ return $message;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/http/Route.php b/libraries/lithium/http/Route.php
new file mode 100644
index 0000000..ba63f27
--- /dev/null
+++ b/libraries/lithium/http/Route.php
@@ -0,0 +1,233 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+
+class Route extends \lithium\core\Object {
+
+ /**
+ * The URL template string that the route matches. 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}/**"`.
+ *
+ * @var string
+ */
+ protected $_template = '';
+
+ /**
+ * The regular expression used to match URLs. This regular expression is typically 'compiled'
+ * down from the higher-level syntax used in `$_template`, but can be set manually with
+ * compilation turned off in the constructor for extra control or if you are using pre-compiled
+ * `Route` objects
+ *
+ * @var string
+ * @see lithium\http\Route::$_template
+ * @see lithium\http\Route::__construct()
+ */
+ protected $_pattern = '';
+
+ protected $_keys = array();
+
+ protected $_params = array();
+
+ protected $_match = array();
+
+ protected $_defaults = array();
+
+ protected $_subPatterns = array();
+
+ protected $_autoConfig = array(
+ 'template', 'pattern', 'keys', 'params', 'match', 'defaults', 'subPatterns'
+ );
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'params' => array(),
+ 'template' => '/',
+ 'pattern' => '^[\/]*$',
+ 'match' => array(),
+ 'defaults' => array(),
+ 'keys' => array(),
+ 'options' => array()
+ );
+ parent::__construct((array)$config + $defaults);
+ }
+
+ protected function _init() {
+ parent::_init();
+ $this->_pattern = $this->_pattern ?: rtrim($this->_template, '/');
+ $this->_params += array('action' => 'index');
+ $this->compile($this->_config['options']);
+ }
+
+ /**
+ * Attempts to parse a request object and determine its execution details.
+ *
+ * @param object $request A request object, usually an instance of `lithium\http\Request`,
+ * containing the details of the request to be routed.
+ * @return mixed If this route matches `$request`, returns an array of the execution details
+ * contained in the route, otherwise returns false.
+ */
+ 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;
+ }
+ return false;
+ }
+
+ /**
+ * Matches a set of parameters against the route, and returns a URL string if the route matches
+ * the parameters, or false if it does not match.
+ *
+ * @param string $options
+ * @param string $context
+ * @return mixed
+ */
+ public function match($options = array(), $context = null) {
+ $defaults = array('action' => 'index');
+ $options += $defaults;
+
+ 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;
+ }
+ return $this->_write($options, $defaults + $this->_defaults + array('args' => ''));
+ }
+
+ /**
+ * Writes a set of URL options to this route's template string.
+ *
+ * @param array $options The options to write to this route, with defaults pre-merged.
+ * @param array $defaults The default template options for this route (contains hard-coded
+ * default values).
+ * @return string Returns the route template string with option values inserted.
+ */
+ protected function _write($options, $defaults) {
+ $template = $this->_template;
+ $trimmed = true;
+
+ if (isset($options['args']) && is_array($options['args'])) {
+ $options['args'] = join('/', $options['args']);
+ }
+
+ foreach (array_reverse($options + array('args' => ''), true) as $key => $value) {
+ $rpl = "{:{$key}}";
+ $len = -strlen($rpl);
+
+ if ($trimmed && array_key_exists($key, $defaults) && $value == $defaults[$key]) {
+ if (substr($template, $len) == $rpl) {
+ $template = rtrim(substr($template, 0, $len), '/');
+ continue;
+ }
+ }
+ $template = str_replace($rpl, $value, $template);
+ $trimmed = ($key == 'args') ? $trimmed : false;
+ }
+ return $template;
+ }
+
+ /**
+ * Exports the properties that make up the route to an array, for debugging, caching or
+ * introspection purposes.
+ *
+ * @return array An array containing the properties of the route object, such as URL templates
+ * and parameter lists.
+ */
+ public function export() {
+ $result = array();
+ $keys = array('template', 'pattern', 'keys', 'params', 'match', 'defaults', 'subPatterns');
+
+ foreach ($keys as $key) {
+ $result[$key] = $this->{'_' . $key};
+ }
+ return $result;
+ }
+
+ /**
+ * Compiles URL templates into regular expression patterns for matching against request URLs,
+ * and extracts template parameters into match-parameter arrays.
+ *
+ * @return void
+ */
+ public function compile($options = array()) {
+ $defaults = array('wrap' => true, 'compile' => true);
+ $options += $defaults;
+
+ if (!$options['compile']) {
+ $this->_pattern = $options['wrap'] ? '@^' . $this->_pattern . '$@' : $this->_pattern;
+ return;
+ }
+
+ $this->_match = $this->_params;
+ $this->_pattern = $this->_template;
+ $this->_pattern = $options['wrap'] ? '@^' . $this->_pattern . '$@' : $this->_pattern;
+
+ if ($this->_template === '/' || $this->_template === '') {
+ return;
+ }
+ preg_match_all('/(?:\{:(?P<params>[^}]+)\})/', $this->_pattern, $keys);
+
+ if (empty($keys['params'])) {
+ return;
+ }
+ $shortKeys = array();
+ $this->_pattern = str_replace('.{', '\.{', $this->_pattern);
+
+ if (strpos($this->_pattern, '{:args}') !== false) {
+ $this->_pattern = str_replace('/{:args}', '(?:/(?P<args>.*))?', $this->_pattern);
+ $this->_pattern = str_replace('{:args}', '(?:/(?P<args>.*))?', $this->_pattern);
+ $this->_keys['args'] = 'args';
+ }
+
+ foreach ($keys['params'] as $i => $param) {
+ $_param = preg_quote($param, '@');
+
+ if (strpos($param, ':')) {
+ list($param, $pattern) = explode(':', $param, 2);
+ $this->_subPatterns[$param] = $pattern;
+ $shortKeys[$i] = $param;
+ } else {
+ $pattern = '[^\/]+';
+ }
+ $req = (array_key_exists($param, $this->_params) ? '?' : '');
+
+ $regex = "(?P<{$param}>{$pattern}){$req}";
+ $this->_pattern = str_replace("/{:{$_param}}", "(?:/{$regex}){$req}", $this->_pattern);
+ $this->_pattern = str_replace("{:{$_param}}", $regex, $this->_pattern);
+ }
+ $shortKeys += $keys['params'];
+ 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);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/http/Router.php b/libraries/lithium/http/Router.php
new file mode 100644
index 0000000..31dd51c
--- /dev/null
+++ b/libraries/lithium/http/Router.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+
+use \lithium\util\Collection;
+
+class Router extends \lithium\core\StaticObject {
+
+ protected static $_configuration = null;
+
+ protected static $_classes = array(
+ 'route' => '\lithium\http\Route'
+ );
+
+ public static function __init() {
+ static::$_configuration = new Collection();
+ }
+
+ /**
+ * Connects a new route and returns the current routes array.
+ *
+ * @param string $route An empty string, or a route string "/"
+ * @param array $params An array describing the default or required elements of the route
+ * @see lithium\http\Router::parse()
+ * @return array Array of routes
+ */
+ public static function connect($template, $params = array(), $options = array()) {
+ if ($template === null) {
+ return static::__init();
+ }
+
+ if (!is_object($template)) {
+ $params + array('action' => 'index');
+ $class = static::$_classes['route'];
+ $template = new $class(compact('template', 'params', 'options'));
+ }
+ return (static::$_configuration[] = $template);
+ }
+
+ /**
+ * Takes an instance of lithium\http\Request (or a subclass) and matches it against each
+ * route, in the order that the routes are connected.
+ *
+ * @param object $request A request object containing URL and environment data
+ * @return array
+ * @see lithium\http\Router::connect()
+ */
+ public static function parse($request) {
+ return static::$_configuration->first(function($route) use ($request) {
+ return $route->parse($request);
+ });
+ }
+
+ 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;
+ }
+ $base = isset($context) ? $context->env('base') : '';
+ $path = trim($path, '/');
+ return "{$base}/{$path}";
+ }
+ $defaults = array('action' => 'index');
+ $options += $defaults;
+ $base = isset($context) ? $context->env('base') : '';
+
+ return $base . static::$_configuration->first(function($route) use ($options, $context) {
+ return $route->match($options, $context);
+ });
+ }
+
+ public static function get($route = null) {
+ if (empty(static::$_configuration)) {
+ static::__init();
+ }
+ if (is_null($route)) {
+ return static::$_configuration;
+ }
+ return isset(static::$_configuration[$route]) ? static::$_configuration[$route] : null;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/http/Socket.php b/libraries/lithium/http/Socket.php
new file mode 100644
index 0000000..d08b115
--- /dev/null
+++ b/libraries/lithium/http/Socket.php
@@ -0,0 +1,953 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\http;
+
+use \lithium\util\Set;
+
+class Socket extends \lithium\core\Object {
+
+ /**
+ * When one activates the $quirksMode by setting it to true, all checks meant to enforce RFC
+ * 2616 (HTTP/1.1 specs) will be disabled and additional measures to deal with non-standard
+ * responses will be enabled.
+ *
+ * @var boolean
+ */
+ public $quirksMode = false;
+
+ /**
+ * The default values to use for a request.
+ *
+ * @var array
+ */
+ public $request = array(
+ 'method' => 'GET',
+ 'uri' => array(
+ 'scheme' => 'http',
+ 'host' => null,
+ 'port' => 80,
+ 'user' => null,
+ 'pass' => null,
+ 'path' => null,
+ 'query' => null,
+ 'fragment' => null
+ ),
+ 'auth' => array(
+ 'method' => 'Basic',
+ 'user' => null,
+ 'pass' => null
+ ),
+ 'version' => '1.1',
+ 'body' => '',
+ 'line' => null,
+ 'header' => array(
+ 'Connection' => 'close',
+ 'User-Agent' => 'Lithium'
+ ),
+ 'raw' => null,
+ 'cookies' => array()
+ );
+
+ /**
+ * The default structure for storing the response
+ *
+ * @var array
+ */
+ public $response = array(
+ 'raw' => array(
+ 'status-line' => null,
+ 'header' => null,
+ 'body' => null,
+ 'response' => null
+ ),
+ 'status' => array(
+ 'http-version' => null,
+ 'code' => null,
+ 'reason-phrase' => null
+ ),
+ 'header' => array(),
+ 'body' => '',
+ 'cookies' => array()
+ );
+
+ /**
+ * Default configuration settings for Socket.
+ *
+ * @var array
+ */
+ public $config = array(
+ 'persistent' => false,
+ 'host' => 'localhost',
+ 'protocol' => 'tcp',
+ 'port' => 80,
+ 'timeout' => 30,
+ 'request' => array(
+ 'uri' => array(
+ 'scheme' => 'http',
+ 'host' => 'localhost',
+ 'port' => 80
+ ),
+ 'auth' => array(
+ 'method' => 'Basic',
+ 'user' => null,
+ 'pass' => null
+ ),
+ 'cookies' => array()
+ )
+ );
+
+ /**
+ * String that represents a line break.
+ *
+ * @var string
+ */
+ public $lineBreak = "\r\n";
+
+ /**
+ * Build an HTTP Socket using the specified configuration.
+ *
+ * @param array $config Configuration
+ */
+ public function __construct($config = array()) {
+ if (is_string($config)) {
+ $this->configUri($config);
+ } elseif (is_array($config)) {
+ if (isset($config['request']['uri']) && is_string($config['request']['uri'])) {
+ $this->configUri($config['request']['uri']);
+ unset($config['request']['uri']);
+ }
+ $this->config = Set::merge($this->config, $config);
+ }
+ parent::__construct($this->config);
+ }
+
+ /**
+ * Issue the specified request.
+ *
+ * @param mixed $request Either an URI string, or an array defining host/uri
+ * @return mixed false on error, request body on success
+ */
+ public function request($request = array()) {
+ $this->reset(false);
+
+ if (is_string($request)) {
+ $request = array('uri' => $request);
+ } elseif (!is_array($request)) {
+ return false;
+ }
+
+ if (!isset($request['uri'])) {
+ $request['uri'] = null;
+ }
+ $uri = $this->parseUri($request['uri']);
+
+ if (!isset($uri['host'])) {
+ $host = $this->config['host'];
+ }
+ if (isset($request['host'])) {
+ $host = $request['host'];
+ unset($request['host']);
+ }
+
+ $request['uri'] = $this->url($request['uri']);
+ $request['uri'] = $this->parseUri($request['uri'], true);
+ $this->request = Set::merge($this->request, $this->config['request'], $request);
+
+ $this->configUri($this->request['uri']);
+
+ if (isset($host)) {
+ $this->config['host'] = $host;
+ }
+ $cookies = null;
+
+ if (is_array($this->request['header'])) {
+ $this->request['header'] = $this->parseHeader($this->request['header']);
+ if (!empty($this->request['cookies'])) {
+ $cookies = $this->buildCookies($this->request['cookies']);
+ }
+ $this->request['header'] = array_merge(array('Host' => $this->request['uri']['host']), $this->request['header']);
+ }
+
+ if (isset($this->request['auth']['user']) && isset($this->request['auth']['pass'])) {
+ $this->request['header']['Authorization'] = $this->request['auth']['method'] ." ". base64_encode($this->request['auth']['user'] .":".$this->request['auth']['pass']);
+ }
+ if (isset($this->request['uri']['user']) && isset($this->request['uri']['pass'])) {
+ $this->request['header']['Authorization'] = $this->request['auth']['method'] ." ". base64_encode($this->request['uri']['user'] .":".$this->request['uri']['pass']);
+ }
+
+ if (is_array($this->request['body'])) {
+ $this->request['body'] = $this->httpSerialize($this->request['body']);
+ }
+
+ if (!empty($this->request['body']) && !isset($this->request['header']['Content-Type'])) {
+ $this->request['header']['Content-Type'] = 'application/x-www-form-urlencoded';
+ }
+
+ if (!empty($this->request['body']) && !isset($this->request['header']['Content-Length'])) {
+ $this->request['header']['Content-Length'] = strlen($this->request['body']);
+ }
+
+ $connectionType = @$this->request['header']['Connection'];
+ $this->request['header'] = $this->buildHeader($this->request['header']) . $cookies;
+
+ if (empty($this->request['line'])) {
+ $this->request['line'] = $this->buildRequestLine($this->request);
+ }
+
+ if ($this->quirksMode === false && $this->request['line'] === false) {
+ return $this->response = false;
+ }
+
+ if ($this->request['line'] !== false) {
+ $this->request['raw'] = $this->request['line'];
+ }
+
+ if ($this->request['header'] !== false) {
+ $this->request['raw'] .= $this->request['header'];
+ }
+
+ $this->request['raw'] .= "\r\n";
+ $this->request['raw'] .= $this->request['body'];
+ $this->write($this->request['raw']);
+
+ $response = null;
+ while ($data = $this->read()) {
+ $response .= $data;
+ }
+
+ if ($connectionType == 'close') {
+ $this->disconnect();
+ }
+ $this->response = $this->parseResponse($response);
+
+ if (!empty($this->response['cookies'])) {
+ $this->config['request']['cookies'] = array_merge(
+ $this->config['request']['cookies'],
+ $this->response['cookies']
+ );
+ }
+ return $this->response['body'];
+ }
+
+ /**
+ * Issues a GET request to the specified URI, query, and request.
+ *
+ * @param mixed $uri URI to request (see {@link parseUri()})
+ * @param array $query Query to append to URI
+ * @param array $request An indexed array with indexes such as 'method' or uri
+ * @return mixed Result of request
+ */
+ public function get($uri = null, $query = array(), $request = array()) {
+ if (!empty($query)) {
+ $uri =$this->parseUri($uri);
+ if (isset($uri['query'])) {
+ $uri['query'] = array_merge($uri['query'], $query);
+ } else {
+ $uri['query'] = $query;
+ }
+ $uri = $this->buildUri($uri);
+ }
+ $request = Set::merge(array('method' => 'GET', 'uri' => $uri), $request);
+ return $this->request($request);
+ }
+
+ /**
+ * Issues a POST request to the specified URI, query, and request.
+ *
+ * @param mixed $uri URI to request (see {@link parseUri()})
+ * @param array $query Query to append to URI
+ * @param array $request An indexed array with indexes such as 'method' or uri
+ * @return mixed Result of request
+ */
+ public function post($uri = null, $data = array(), $request = array()) {
+ $request = Set::merge(array('method' => 'POST', 'uri' => $uri, 'body' => $data), $request);
+ return $this->request($request);
+ }
+
+ /**
+ * Issues a PUT request to the specified URI, query, and request.
+ *
+ * @param mixed $uri URI to request (see {@link parseUri()})
+ * @param array $query Query to append to URI
+ * @param array $request An indexed array with indexes such as 'method' or uri
+ * @return mixed Result of request
+ */
+ public function put($uri = null, $data = array(), $request = array()) {
+ $request = Set::merge(array('method' => 'PUT', 'uri' => $uri, 'body' => $data), $request);
+ return $this->request($request);
+ }
+
+ /**
+ * Issues a DELETE request to the specified URI, query, and request.
+ *
+ * @param mixed $uri URI to request (see {@link parseUri()})
+ * @param array $query Query to append to URI
+ * @param array $request An indexed array with indexes such as 'method' or uri
+ * @return mixed Result of request
+ */
+ public function delete($uri = null, $data = array(), $request = array()) {
+ $request = Set::merge(array('method' => 'DELETE', 'uri' => $uri, 'body' => $data), $request);
+ return $this->request($request);
+ }
+
+ /**
+ * undocumented function
+ *
+ * @param unknown $url
+ * @param unknown $uriTemplate
+ * @return void
+ */
+ public function url($url = null, $uriTemplate = null) {
+ if (is_null($url)) {
+ $url = '/';
+ }
+ if (is_string($url)) {
+ if ($url{0} == '/') {
+ $url = $this->config['request']['uri']['host'].':'.$this->config['request']['uri']['port'] . $url;
+ }
+ if (!preg_match('/^.+:\/\/|\*|^\//', $url)) {
+ $url = $this->config['request']['uri']['scheme'].'://'.$url;
+ }
+ } elseif (!is_array($url) && !empty($url)) {
+ return false;
+ }
+
+ $base = array_merge($this->config['request']['uri'], array('scheme' => array('http', 'https'), 'port' => array(80, 443)));
+ $url = $this->parseUri($url, $base);
+
+ if (empty($url)) {
+ $url = $this->config['request']['uri'];
+ }
+
+ if (!empty($uriTemplate)) {
+ return $this->buildUri($url, $uriTemplate);
+ }
+ return $this->buildUri($url);
+ }
+
+ /**
+ * Parses the given message and breaks it down in parts.
+ *
+ * @param string $message Message to parse
+ * @return array Parsed message (with indexed elements such as raw, status, header, body)
+ */
+ protected function parseResponse($message) {
+ if (is_array($message)) {
+ return $message;
+ } elseif (!is_string($message)) {
+ return false;
+ }
+
+ static $responseTemplate;
+
+ if (empty($responseTemplate)) {
+ $classVars = get_class_vars(__CLASS__);
+ $responseTemplate = $classVars['response'];
+ }
+
+ $response = $responseTemplate;
+
+ if (!preg_match("/^(.+\r\n)(.*)(?<=\r\n)\r\n/Us", $message, $match)) {
+ return false;
+ }
+
+ list($null, $response['raw']['status-line'], $response['raw']['header']) = $match;
+ $response['raw']['response'] = $message;
+ $response['raw']['body'] = substr($message, strlen($match[0]));
+
+ if (preg_match("/(.+) ([0-9]{3}) (.+)\r\n/DU", $response['raw']['status-line'], $match)) {
+ $response['status']['http-version'] = $match[1];
+ $response['status']['code'] = (int)$match[2];
+ $response['status']['reason-phrase'] = $match[3];
+ }
+
+ $response['header'] = $this->parseHeader($response['raw']['header']);
+ $decoded = $this->decodeBody($response['raw']['body'], @$response['header']['Transfer-Encoding']);
+ $response['body'] = $decoded['body'];
+
+ if (!empty($decoded['header'])) {
+ $response['header'] = $this->parseHeader($this->buildHeader($response['header']).$this->buildHeader($decoded['header']));
+ }
+
+ if (!empty($response['header'])) {
+ $response['cookies'] = $this->parseCookies($response['header']);
+ }
+
+ foreach ($response['raw'] as $field => $val) {
+ if ($val === '') {
+ $response['raw'][$field] = null;
+ }
+ }
+
+ return $response;
+ }
+
+ /**
+ * Generic function to decode a $body with a given $encoding. Returns either an array with the keys
+ * 'body' and 'header' or false on failure.
+ *
+ * @param string $body A string continaing the body to decode
+ * @param mixed $encoding Can be false in case no encoding is being used, or a string representing the encoding
+ * @return mixed Array or false
+ */
+ protected function decodeBody($body, $encoding = 'chunked') {
+ if (!is_string($body)) {
+ return false;
+ }
+ if (empty($encoding)) {
+ return array('body' => $body, 'header' => false);
+ }
+ $decodeMethod = 'decode'.Inflector::camelize(str_replace('-', '_', $encoding)).'Body';
+
+ if (!is_callable(array(&$this, $decodeMethod))) {
+ if (!$this->quirksMode) {
+ trigger_error(sprintf(__('HttpSocket::decodeBody - Unknown encoding: %s. Activate quirks mode to surpress error.', true), h($encoding)), E_USER_WARNING);
+ }
+ return array('body' => $body, 'header' => false);
+ }
+ return $this->{$decodeMethod}($body);
+ }
+
+ /**
+ * Decodes a chunked message $body and returns either an array with the keys 'body' and 'header' or false as
+ * a result.
+ *
+ * @param string $body A string continaing the chunked body to decode
+ * @return mixed Array or false
+ */
+ protected function decodeChunkedBody($body) {
+ if (!is_string($body)) {
+ return false;
+ }
+
+ $decodedBody = null;
+ $chunkLength = null;
+
+ while ($chunkLength !== 0) {
+ if (!preg_match("/^([0-9a-f]+) *(?:;(.+)=(.+))?\r\n/iU", $body, $match)) {
+ if (!$this->quirksMode) {
+ trigger_error(__('HttpSocket::decodeChunkedBody - Could not parse malformed chunk. Activate quirks mode to do this.', true), E_USER_WARNING);
+ return false;
+ }
+ break;
+ }
+
+ $chunkSize = 0;
+ $hexLength = 0;
+ $chunkExtensionName = '';
+ $chunkExtensionValue = '';
+ if (isset($match[0])) {
+ $chunkSize = $match[0];
+ }
+ if (isset($match[1])) {
+ $hexLength = $match[1];
+ }
+ if (isset($match[2])) {
+ $chunkExtensionName = $match[2];
+ }
+ if (isset($match[3])) {
+ $chunkExtensionValue = $match[3];
+ }
+
+ $body = substr($body, strlen($chunkSize));
+ $chunkLength = hexdec($hexLength);
+ $chunk = substr($body, 0, $chunkLength);
+ if (!empty($chunkExtensionName)) {
+ /**
+ * @todo See if there are popular chunk extensions we should implement
+ */
+ }
+ $decodedBody .= $chunk;
+ if ($chunkLength !== 0) {
+ $body = substr($body, $chunkLength+strlen("\r\n"));
+ }
+ }
+
+ $entityHeader = false;
+ if (!empty($body)) {
+ $entityHeader = $this->parseHeader($body);
+ }
+ return array('body' => $decodedBody, 'header' => $entityHeader);
+ }
+
+ /**
+ * Parses and sets the specified URI into current request configuration.
+ *
+ * @param mixed $uri URI (see {@link parseUri()})
+ * @return array Current configuration settings
+ */
+ protected function configUri($uri = null) {
+ if (empty($uri)) {
+ return false;
+ }
+
+ if (is_array($uri)) {
+ $uri = $this->parseUri($uri);
+ } else {
+ $uri = $this->parseUri($uri, true);
+ }
+
+ if (!isset($uri['host'])) {
+ return false;
+ }
+
+ $config = array(
+ 'request' => array(
+ 'uri' => array_intersect_key($uri, $this->config['request']['uri']),
+ 'auth' => array_intersect_key($uri, $this->config['request']['auth'])
+ )
+ );
+ $this->config = Set::merge($this->config, $config);
+ $this->config = Set::merge($this->config, array_intersect_key($this->config['request']['uri'], $this->config));
+ return $this->config;
+ }
+
+ /**
+ * Takes a $uri array and turns it into a fully qualified URL string
+ *
+ * @param array $uri A $uri array, or uses $this->config if left empty
+ * @param string $uriTemplate The Uri template/format to use
+ * @return string A fully qualified URL formated according to $uriTemplate
+ */
+ protected function buildUri($uri = array(), $uriTemplate = '%scheme://%user:%pass@%host:%port/%path?%query#%fragment') {
+ if (is_string($uri)) {
+ $uri = array('host' => $uri);
+ }
+ $uri = $this->parseUri($uri, true);
+
+ if (!is_array($uri) || empty($uri)) {
+ return false;
+ }
+
+ $uri['path'] = preg_replace('/^\//', null, $uri['path']);
+ $uri['query'] = $this->httpSerialize($uri['query']);
+ $stripIfEmpty = array(
+ 'query' => '?%query',
+ 'fragment' => '#%fragment',
+ 'user' => '%user:%pass@'
+ );
+
+ foreach ($stripIfEmpty as $key => $strip) {
+ if (empty($uri[$key])) {
+ $uriTemplate = str_replace($strip, null, $uriTemplate);
+ }
+ }
+
+ $defaultPorts = array('http' => 80, 'https' => 443);
+ if (array_key_exists($uri['scheme'], $defaultPorts) && $defaultPorts[$uri['scheme']] == $uri['port']) {
+ $uriTemplate = str_replace(':%port', null, $uriTemplate);
+ }
+
+ foreach ($uri as $property => $value) {
+ $uriTemplate = str_replace('%'.$property, $value, $uriTemplate);
+ }
+
+ if ($uriTemplate === '/*') {
+ $uriTemplate = '*';
+ }
+ return $uriTemplate;
+ }
+
+ /**
+ * Parses the given URI and breaks it down into pieces as an indexed array with elements
+ * such as 'scheme', 'port', 'query'.
+ *
+ * @param string $uri URI to parse
+ * @param mixed $base If true use default URI config, otherwise indexed array to set 'scheme', 'host', 'port', etc.
+ * @return array Parsed URI
+ */
+ protected function parseUri($uri = null, $base = array()) {
+ $uriBase = array(
+ 'scheme' => array('http', 'https'),
+ 'host' => null,
+ 'port' => array(80, 443),
+ 'user' => null,
+ 'pass' => null,
+ 'path' => '/',
+ 'query' => null,
+ 'fragment' => null
+ );
+
+ if (is_string($uri)) {
+ $uri = parse_url($uri);
+ }
+ if (!is_array($uri) || empty($uri)) {
+ return false;
+ }
+ if ($base === true) {
+ $base = $uriBase;
+ }
+
+ if (isset($base['port'], $base['scheme']) && is_array($base['port']) && is_array($base['scheme'])) {
+ if (isset($uri['scheme']) && !isset($uri['port'])) {
+ $base['port'] = $base['port'][array_search($uri['scheme'], $base['scheme'])];
+ } elseif (isset($uri['port']) && !isset($uri['scheme'])) {
+ $base['scheme'] = $base['scheme'][array_search($uri['port'], $base['port'])];
+ }
+ }
+
+ if (is_array($base) && !empty($base)) {
+ $uri = array_merge($base, $uri);
+ }
+
+ if (isset($uri['scheme']) && is_array($uri['scheme'])) {
+ $uri['scheme'] = array_shift($uri['scheme']);
+ }
+ if (isset($uri['port']) && is_array($uri['port'])) {
+ $uri['port'] = array_shift($uri['port']);
+ }
+
+ if (array_key_exists('query', $uri)) {
+ $uri['query'] = $this->parseQuery($uri['query']);
+ }
+
+ if (!array_intersect_key($uriBase, $uri)) {
+ return false;
+ }
+ return $uri;
+ }
+
+ /**
+ * This function can be thought of as a reverse to PHP5's http_build_query(). It takes a given query string and turns it into an array and
+ * supports nesting by using the php bracket syntax. So this menas you can parse queries like:
+ *
+ * - ?key[subKey]=value
+ * - ?key[]=value1&key[]=value2
+ *
+ * A leading '?' mark in $query is optional and does not effect the outcome of this function. For the complete capabilities of this implementation
+ * take a look at HttpSocketTest::testParseQuery()
+ *
+ * @param mixed $query A query string to parse into an array or an array to return directly "as is"
+ * @return array The $query parsed into a possibly multi-level array. If an empty $query is given, an empty array is returned.
+ */
+ protected function parseQuery($query) {
+ if (is_array($query)) {
+ return $query;
+ }
+ $parsedQuery = array();
+
+ if (is_string($query) && !empty($query)) {
+ $query = preg_replace('/^\?/', '', $query);
+ $items = explode('&', $query);
+
+ foreach ($items as $item) {
+ if (strpos($item, '=') !== false) {
+ list($key, $value) = explode('=', $item);
+ } else {
+ $key = $item;
+ $value = null;
+ }
+
+ $key = urldecode($key);
+ $value = urldecode($value);
+
+ if (preg_match_all('/\[([^\[\]]*)\]/iUs', $key, $matches)) {
+ $subKeys = $matches[1];
+ $rootKey = substr($key, 0, strpos($key, '['));
+ if (!empty($rootKey)) {
+ array_unshift($subKeys, $rootKey);
+ }
+ $queryNode =& $parsedQuery;
+
+ foreach ($subKeys as $subKey) {
+ if (!is_array($queryNode)) {
+ $queryNode = array();
+ }
+
+ if ($subKey === '') {
+ $queryNode[] = array();
+ end($queryNode);
+ $subKey = key($queryNode);
+ }
+ $queryNode =& $queryNode[$subKey];
+ }
+ $queryNode = $value;
+ } else {
+ $parsedQuery[$key] = $value;
+ }
+ }
+ }
+ return $parsedQuery;
+ }
+
+ /**
+ * Builds a request line according to HTTP/1.1 specs. Activate quirks mode to work outside specs.
+ *
+ * @param array $request Needs to contain a 'uri' key. Should also contain a 'method' key, otherwise defaults to GET.
+ * @param string $versionToken The version token to use, defaults to HTTP/1.1
+ * @return string Request line
+ */
+ protected function buildRequestLine($request = array(), $versionToken = 'HTTP/1.1') {
+ $asteriskMethods = array('OPTIONS');
+
+ if (is_string($request)) {
+ $isValid = preg_match("/(.+) (.+) (.+)\r\n/U", $request, $match);
+ if (!$this->quirksMode && (!$isValid || ($match[2] == '*' && !in_array($match[3], $asteriskMethods)))) {
+ trigger_error(__('HttpSocket::buildRequestLine - Passed an invalid request line string. Activate quirks mode to do this.', true), E_USER_WARNING);
+ return false;
+ }
+ return $request;
+ } elseif (!is_array($request)) {
+ return false;
+ } elseif (!array_key_exists('uri', $request)) {
+ return false;
+ }
+
+ $request['uri'] = $this->parseUri($request['uri']);
+ $request = array_merge(array('method' => 'GET'), $request);
+ $request['uri'] = $this->buildUri($request['uri'], '/%path?%query');
+
+ if (!$this->quirksMode && $request['uri'] === '*' && !in_array($request['method'], $asteriskMethods)) {
+ trigger_error(sprintf(__('HttpSocket::buildRequestLine - The "*" asterisk character is only allowed for the following methods: %s. Activate quirks mode to work outside of HTTP/1.1 specs.', true), join(',', $asteriskMethods)), E_USER_WARNING);
+ return false;
+ }
+ return $request['method'].' '.$request['uri'].' '.$versionToken.$this->lineBreak;
+ }
+
+ /**
+ * Serializes an array for transport.
+ *
+ * @param array $data Data to serialize
+ * @return string Serialized variable
+ */
+ protected function httpSerialize($data = array()) {
+ if (is_string($data)) {
+ return $data;
+ }
+ if (empty($data) || !is_array($data)) {
+ return false;
+ }
+ return substr(Router::queryString($data), 1);
+ }
+
+ /**
+ * Builds the header.
+ *
+ * @param array $header Header to build
+ * @return string Header built from array
+ */
+ protected function buildHeader($header, $mode = 'standard') {
+ if (is_string($header)) {
+ return $header;
+ } elseif (!is_array($header)) {
+ return false;
+ }
+
+ $returnHeader = '';
+ foreach ($header as $field => $contents) {
+ if (is_array($contents) && $mode == 'standard') {
+ $contents = join(',', $contents);
+ }
+ foreach ((array)$contents as $content) {
+ $contents = preg_replace("/\r\n(?![\t ])/", "\r\n ", $content);
+ $field = $this->escapeToken($field);
+
+ $returnHeader .= $field.': '.$contents.$this->lineBreak;
+ }
+ }
+ return $returnHeader;
+ }
+
+ /**
+ * Parses an array based header.
+ *
+ * @param array $header Header as an indexed array (field => value)
+ * @return array Parsed header
+ */
+ protected function parseHeader($header) {
+ if (is_array($header)) {
+ foreach ($header as $field => $value) {
+ unset($header[$field]);
+ $field = strtolower($field);
+ preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE);
+
+ foreach ($offsets[0] as $offset) {
+ $field = substr_replace($field, strtoupper($offset[0]), $offset[1], 1);
+ }
+ $header[$field] = $value;
+ }
+ return $header;
+ } elseif (!is_string($header)) {
+ return false;
+ }
+
+ preg_match_all("/(.+):(.+)(?:(?<![\t ])".$this->lineBreak."|\$)/Uis", $header, $matches, PREG_SET_ORDER);
+
+ $header = array();
+ foreach ($matches as $match) {
+ list(, $field, $value) = $match;
+
+ $value = trim($value);
+ $value = preg_replace("/[\t ]\r\n/", "\r\n", $value);
+
+ $field = $this->unescapeToken($field);
+
+ $field = strtolower($field);
+ preg_match_all('/(?:^|(?<=-))[a-z]/U', $field, $offsets, PREG_OFFSET_CAPTURE);
+ foreach ($offsets[0] as $offset) {
+ $field = substr_replace($field, strtoupper($offset[0]), $offset[1], 1);
+ }
+
+ if (!isset($header[$field])) {
+ $header[$field] = $value;
+ } else {
+ $header[$field] = array_merge((array)$header[$field], (array)$value);
+ }
+ }
+ return $header;
+ }
+
+ /**
+ * undocumented function
+ *
+ * @param unknown $header
+ * @return void
+ * @todo Make this 100% RFC 2965 confirm
+ */
+ public function parseCookies($header) {
+ if (!isset($header['Set-Cookie'])) {
+ return false;
+ }
+
+ $cookies = array();
+ foreach ((array)$header['Set-Cookie'] as $cookie) {
+ $parts = preg_split('/(?<![^;]");[ \t]*/', $cookie);
+ list($name, $value) = explode('=', array_shift($parts));
+ $cookies[$name] = compact('value');
+ foreach ($parts as $part) {
+ if (strpos($part, '=') !== false) {
+ list($key, $value) = explode('=', $part);
+ } else {
+ $key = $part;
+ $value = true;
+ }
+
+ $key = strtolower($key);
+ if (!isset($cookies[$name][$key])) {
+ $cookies[$name][$key] = $value;
+ }
+ }
+ }
+ return $cookies;
+ }
+
+ /**
+ * undocumented function
+ *
+ * @param unknown $cookies
+ * @return void
+ * @todo Refactor token escape mechanism to be configurable
+ */
+ public function buildCookies($cookies) {
+ $header = array();
+ foreach ($cookies as $name => $cookie) {
+ $header[] = $name.'='.$this->escapeToken($cookie['value'], array(';'));
+ }
+ $header = $this->buildHeader(array('Cookie' => $header), 'pragmatic');
+ return $header;
+ }
+
+ /**
+ * undocumented function
+ *
+ * @return void
+ */
+ public function saveCookies() {
+ }
+
+ /**
+ * undocumented function
+ *
+ * @return void
+ */
+ public function loadCookies() {
+ }
+
+ /**
+ * Unescapes a given $token according to RFC 2616 (HTTP 1.1 specs)
+ *
+ * @param string $token Token to unescape
+ * @return string Unescaped token
+ * @todo Test $chars parameter
+ */
+ protected function unescapeToken($token, $chars = null) {
+ $regex = '/"(['.join('', $this->__tokenEscapeChars(true, $chars)).'])"/';
+ $token = preg_replace($regex, '\\1', $token);
+ return $token;
+ }
+
+ /**
+ * Escapes a given $token according to RFC 2616 (HTTP 1.1 specs)
+ *
+ * @param string $token Token to escape
+ * @return string Escaped token
+ * @todo Test $chars parameter
+ */
+ protected function escapeToken($token, $chars = null) {
+ $regex = '/(['.join('', $this->__tokenEscapeChars(true, $chars)).'])/';
+ $token = preg_replace($regex, '"\\1"', $token);
+ return $token;
+ }
+
+ /**
+ * Gets escape chars according to RFC 2616 (HTTP 1.1 specs).
+ *
+ * @param boolean $hex true to get them as HEX values, false otherwise
+ * @return array Escape chars
+ * @access private
+ * @todo Test $chars parameter
+ */
+ public function __tokenEscapeChars($hex = true, $chars = null) {
+ if (!empty($chars)) {
+ $escape = $chars;
+ } else {
+ $escape = array('"', "(", ")", "<", ">", "@", ",", ";", ":", "\\", "/", "[", "]", "?", "=", "{", "}", " ");
+ for ($i = 0; $i <= 31; $i++) {
+ $escape[] = chr($i);
+ }
+ $escape[] = chr(127);
+ }
+
+ if ($hex == false) {
+ return $escape;
+ }
+ $regexChars = '';
+ foreach ($escape as $key => $char) {
+ $escape[$key] = '\\x'.str_pad(dechex(ord($char)), 2, '0', STR_PAD_LEFT);
+ }
+ return $escape;
+ }
+
+ /**
+ * Resets the state of this HttpSocket instance to it's initial state (before Object::__construct got executed) or does
+ * the same thing partially for the request and the response property only.
+ *
+ * @param boolean $full If set to false only HttpSocket::response and HttpSocket::request are reseted
+ * @return boolean True on success
+ */
+ public function reset($full = true) {
+ static $initalState = array();
+ if (empty($initalState)) {
+ $initalState = get_class_vars(__CLASS__);
+ }
+
+ if ($full == false) {
+ $this->request = $initalState['request'];
+ $this->response = $initalState['response'];
+ return true;
+ }
+ parent::reset($initalState);
+ return true;
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/Cache.php b/libraries/lithium/storage/Cache.php
new file mode 100644
index 0000000..fb42773
--- /dev/null
+++ b/libraries/lithium/storage/Cache.php
@@ -0,0 +1,174 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage;
+
+use \lithium\util\Inflector;
+
+/**
+ * @todo Perhaps re-implement using stream wrappers, and stream filters for strategies.
+ *
+ **/
+class Cache extends \lithium\core\Adaptable {
+
+
+ /**
+ * Stores configurations for cache adapters
+ *
+ * @var object Collection of cache configurations
+ */
+ protected static $_configurations = null;
+
+ /**
+ * Generates the cache key.
+ *
+ * @param mixed $key A string (or lambda/closure that evaluates to a string)
+ * that will be used as the cache key.
+ * @param array $data If a lambda/closure is used as a key and requires arguments,
+ * pass them in here.
+ * @return string The generated cache key.
+ * @access public
+ */
+ public static function key($key, $data = array()) {
+ $key = is_object($key) ? $key($data) : $key;
+ return Inflector::slug($key);
+ }
+
+ /**
+ * Writes to the specified cache configuration.
+ *
+ * @param string $name Configuration to be used for writing
+ * @param mixed $key Key to uniquely identify the cache entry
+ * @param mixed $data Data to be cached
+ * @param mixed $conditions Conditions for the write operation to proceed
+ * @return boolean True on successful cache write, false otherwise
+ * @strategy
+ * @access public
+ */
+ public static function write($name, $key, $data, $expiry, $conditions = null) {
+ $settings = static::config();
+
+ if (!isset($settings[$name])) {
+ return false;
+ }
+
+ $key = static::key($key);
+ $methods = array($name => static::adapter($name)->write($key, $data, $expiry, $conditions));
+ $result = false;
+
+ foreach ($methods as $name => $method) {
+ $params = compact('key', 'data', 'expiry', 'conditions');
+ $filters = $settings[$name]['filters'];
+ $result = $result || static::_filter('write', $params, $method, $filters);
+ }
+ return $result;
+ }
+
+
+ /**
+ * Reads from the specified cache configuration
+ *
+ * @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
+ * @return mixed Read results on successful cache read, null otherwise
+ */
+ public static function read($name, $key, $conditions = null) {
+ $settings = static::config();
+
+ if (!isset($settings[$name])) {
+ return false;
+ }
+
+ $key = static::key($key);
+ $methods = array($name => static::adapter($name)->read($key, $conditions));
+ $result = false;
+
+ foreach ($methods as $name => $method) {
+ $params = compact('key', 'conditions');
+ $filters = $settings[$name]['filters'];
+ $result = $result || static::_filter('read', $params, $method, $filters);
+ }
+ return $result;
+ }
+
+ /**
+ * Delete a value from the specified cache configuration
+ *
+ * @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
+ * @return boolean True on successful deletion, false otherwise
+ */
+ public static function delete($name, $key, $conditions = null) {
+ $settings = static::config();
+
+ if (!isset($settings[$name])) {
+ return false;
+ }
+
+ $key = static::key($key);
+ $methods = array($name => static::adapter($name)->delete($key, $conditions));
+ $result = false;
+
+ foreach ($methods as $name => $method) {
+ $params = compact('key', 'conditions');
+ $filters = $settings[$name]['filters'];
+ $result = $result || static::_filter('delete', $params, $method, $filters);
+ }
+ return $result;
+ }
+
+ /**
+ * Perform garbage collection on specified cache configuration.
+ *
+ * @param string $name The cache configuration to be cleaned
+ * @return boolean True on successful clean, false otherwise
+ */
+ public static function clean($name) {
+ $settings = static::config();
+
+ if (!isset($settings[$name])) {
+ return false;
+ }
+
+ return static::adapter($name)->clean();
+ }
+
+ /**
+ * Remove all cache keys from specified confiuration.
+ *
+ * @param string $name The cache configuration to be cleared
+ * @return boolean True on successful clearing, false otherwise
+ */
+ public static function clear($name) {
+ $settings = static::config();
+
+ if (!isset($settings[$name])) {
+ return false;
+ }
+
+ return static::adapter($name)->clear();
+ }
+
+ /**
+ * Returns adapter for given named configuration
+ *
+ * @param string $name Name of configuration
+ * @return object Adapter for named configuration
+ */
+ public static function adapter($name) {
+ return static::_adapter('adapters.storage.cache', $name);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/Session.php b/libraries/lithium/storage/Session.php
new file mode 100644
index 0000000..dc83126
--- /dev/null
+++ b/libraries/lithium/storage/Session.php
@@ -0,0 +1,183 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Session extends \lithium\core\Adaptable {
+
+ /**
+ * Stores configurations for cache adapters
+ *
+ * @var object Collection of cache configurations
+ */
+ protected static $_configurations = null;
+
+
+ /**
+ * Returns key to be used in session read, write and delete operations
+ *
+ * @param mixed $name Named session configuration
+ * @return string Key
+ */
+ public static function key($name = null) {
+ return is_object($adapter = static::adapter($name)) ? $adapter->key() : null;
+ }
+
+ /**
+ * Indicates whether the the current request includes information on a previously started
+ * session.
+ *
+ * @return boolean Returns true if a the request includes a key from a previously created
+ * session.
+ */
+ public static function isStarted($name = null) {
+ return is_object($adapter = static::adapter($name)) ? $adapter->isStarted() : false;
+ }
+
+ /**
+ * 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.
+ *
+ * @return boolean Returns true if the current session is active and valid.
+ */
+ public static function isValid($name = null) {
+
+ }
+
+ public static function read($key, $options = array()) {
+ $defaults = array('name' => null);
+ $options += $defaults;
+ $method = ($name = $options['name']) ? static::_adapter($name)->read($key, $options) : null;
+ $settings = static::config();
+
+ if (!$method) {
+ foreach ($settings->keys() as $name) {
+ if ($method = static::adapter($name)->read($key, $options)) {
+ break;
+ }
+ }
+ if (!$method || !$name) {
+ return null;
+ }
+ }
+ $filters = $settings[$name]['filters'];
+ return static::_filter('read', compact('key', 'options'), $method, $filters);
+ }
+
+ /**
+ * Writes a persistent value to one or more session stores.
+ *
+ * @param string $key
+ * @param mixed $value
+ * @param array $options
+ * @return boolean
+ */
+ public static function write($key, $value = null, $options = array()) {
+ $settings = static::config();
+
+ $defaults = array('name' => null);
+ $options += $defaults;
+
+ if (is_resource($value) || !$settings->count()) {
+ return false;
+ }
+ $methods = array();
+
+ if ($name = $options['name']) {
+ $methods = array($name => static::adapter($name)->write($key, $value, $options));
+ } else {
+ foreach ($settings->keys() as $name) {
+ if ($method = static::adapter($name)->write($key, $value, $options)) {
+ $methods[$name] = $method;
+ }
+ }
+ }
+ $result = false;
+
+ foreach ($methods as $name => $method) {
+ $params = compact('key', 'value', 'options');
+ $filters = $settings[$name]['filters'];
+ $result = $result || static::_filter('write', $params, $method, $filters);
+ }
+ return $result;
+ }
+
+ /**
+ * Deletes a named key from a single adapter (if a `'name'` option is specified) or all
+ * session adapters.
+ *
+ * @param string $key The name of the session key to delete
+ * @param array $options
+ * @return void
+ */
+ public static function delete($key, $options = array()) {
+ $defaults = array('name' => null);
+ $options += $defaults;
+ $settings = static::config();
+
+ if ($options['name']) {
+ return static::adapter($options['name'])->delete($key, $options);
+ }
+ foreach ($settings->keys() as $name) {
+ static::adapter($name)->delete($key, $options);
+ }
+ }
+
+ /**
+ * Checks if a session key is set in any adapter, or if a particular adapter configuration is
+ * specified (via `'name'` in `$options`), only that configuration is checked.
+ *
+ * @param string $key The session key to check.
+ * @param array $options
+ * @return boolean
+ */
+ public static function check($key, $options = array()) {
+ $defaults = array('name' => null);
+ $options += $defaults;
+ $settings = static::config();
+
+ if ($options['name']) {
+ return static::adapter($options['name'])->check($key, $options);
+ }
+ foreach ($settings->keys() as $name) {
+ if (static::adapter($name)->check($key, $options)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Clears all named session configurations
+ *
+ * @return void
+ */
+ public static function clear() {
+ return static::reset();
+ }
+
+ /**
+ * Returns adapter for given named configuration
+ *
+ * @param string $name
+ * @return object Adapter for named configuration
+ */
+ public static function adapter($name) {
+ return static::_adapter('adapters.storage.session', $name);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/cache/adapters/Apc.php b/libraries/lithium/storage/cache/adapters/Apc.php
new file mode 100644
index 0000000..ed86b58
--- /dev/null
+++ b/libraries/lithium/storage/cache/adapters/Apc.php
@@ -0,0 +1,92 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\cache\adapters;
+
+/**
+ * Alternative PHP Cache (APC) cache adapter implementation
+ *
+ */
+class Apc extends \lithium\core\Object {
+
+ /**
+ * Class constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ $defaults = array('prefix' => '');
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Write value(s) to the cache
+ *
+ * @param string $key
+ * @param string $value
+ * @param object $conditions
+ * @return boolean True on successful write, false otherwise
+ */
+ public function write($key, $data, $expiry, $conditions = null) {
+ return function($self, $params, $chain) {
+ extract($params);
+ $cachetime = strtotime($expiry);
+ $duration = $cachetime - time();
+
+ apc_store($key . '_expires', $cachetime, $duration);
+ return apc_store($key, $data, $cachetime);
+
+ };
+ }
+
+ /**
+ * Read value(s) from the cache
+ *
+ * @param string $key
+ * @param object $conditions
+ * @return mixed Cached value if successful, false otherwise
+ */
+ public function read($key, $conditions = null) {
+ return function($self, $params, $chain) {
+ extract($params);
+ $cachetime = intval(apc_fetch($key . '_expires'));
+ $time = time();
+ return ($cachetime < $time) ? false : apc_fetch($key);
+ };
+ }
+
+ /**
+ * Delete value from the cache
+ *
+ * @param string $key
+ * @param object $conditions
+ * @return mixed True on successful delete, false otherwise
+ */
+ public function delete($key, $conditions = null) {
+ return function($self, $params, $chain) {
+ extract($params);
+ apc_delete($key . '_expires');
+ return apc_delete($key);
+ };
+ }
+
+ /**
+ * Clears user-space cache
+ *
+ * @return mixed True on successful clear, false otherwise
+ */
+ public function clear() {
+ return apc_clear_cache('user');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/cache/adapters/File.php b/libraries/lithium/storage/cache/adapters/File.php
new file mode 100644
index 0000000..f6adba5
--- /dev/null
+++ b/libraries/lithium/storage/cache/adapters/File.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\cache\adapters;
+
+use \SplFileInfo;
+use \DirectoryIterator;
+
+class File extends \lithium\core\Object {
+
+ /**
+ * Class constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ $defaults = array('path' => LITHIUM_APP_PATH . '/tmp/cache');
+ parent::__construct($config + $defaults);
+ }
+
+ public function write($key, $data, $expiry, $conditions = null) {
+ $path = $this->_config['path'];
+
+ return function($self, $params, $chain) use (&$path) {
+ extract($params);
+ $expiry = strtotime($expiry);
+ $data = "{:expiry:{$expiry}}\n{$data}";
+ $path = "$path/$key";
+
+ return file_put_contents($path, $data);
+ };
+
+ }
+
+ public function read($key, $conditions = null) {
+ $path = $this->_config['path'];
+
+ return function($self, $params, $chain) use (&$path) {
+ extract($params);
+ $path = "$path/$key";
+ $file = new SplFileInfo($path);
+
+ if (!$file->isFile() || !$file->isReadable()) {
+ return false;
+ }
+
+ $data = file_get_contents($path);
+ preg_match('/^\{\:expiry\:(\d+)\}\\n/', $data, $matches);
+ $expiry = $matches[1];
+
+ if ($expiry < time()) {
+ unlink($path);
+ return false;
+ }
+ return preg_replace('/^\{\:expiry\:\d+\}\\n/', '', $data, 1);
+
+ };
+
+ }
+
+ public function delete($key, $conditions = null) {
+ $path = $this->_config['path'];
+
+ return function($self, $params, $chain) use (&$path) {
+ extract($params);
+ $path = "$path/$key";
+ $file = new SplFileInfo($path);
+
+ if ($file->isFile() && $file->isReadable()) {
+ return unlink($path);
+ }
+
+ return false;
+ };
+ }
+
+ public function clear() {
+ $directory = new DirectoryIterator($this->_config['path']);
+
+ foreach ($directory as $file) {
+ if ($file->isFile()) {
+ unlink($file->getPathInfo());
+ }
+ }
+ return true;
+
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/cache/adapters/Memcache.php b/libraries/lithium/storage/cache/adapters/Memcache.php
new file mode 100644
index 0000000..e9b37b7
--- /dev/null
+++ b/libraries/lithium/storage/cache/adapters/Memcache.php
@@ -0,0 +1,124 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\cache\adapters;
+
+use \lithium\util\Set;
+
+/**
+ * libmemcached cache adapter implementation
+ *
+ */
+class Memcache extends \lithium\core\Object {
+
+ /**
+ * Memcache object
+ *
+ * @var object Memcache object
+ */
+ protected static $_Memcached = null;
+
+ /**
+ * Class constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'prefix' => '',
+ 'servers' => array(
+ array('host' => '127.0.0.1', 'port' => 11211, 'weight' => 100)
+ )
+ );
+
+ if (is_null(static::$_Memcached)) {
+ static::$_Memcached = new \Memcached();
+ }
+
+ $configuration = Set::merge($defaults, $config);
+ parent::__construct($configuration);
+
+ foreach ($this->_config['servers'] as $server) {
+ static::$_Memcached->addServer($server['host'], $server['port'], $server['weight']);
+ }
+
+ return extension_loaded('memcached');
+ }
+
+ /**
+ * Write value(s) to the cache
+ *
+ * @param string $key
+ * @param string $value
+ * @param object $conditions
+ * @return boolean True on successful write, false otherwise
+ */
+ public function write($key, $value, $expiry, $conditions = null) {
+ $Memcached =& static::$_Memcached;
+
+ return function($self, $params, $chain) use (&$Memcached) {
+ extract($params);
+ $expires = strtotime($expiry);
+
+ $Memcached->set($key . '_expires', $expires, $expires);
+ return $Memcached->set($key, $data, $expires);
+
+ };
+ }
+
+ /**
+ * Read value(s) from the cache
+ *
+ * @param string $key
+ * @param object $conditions
+ * @return mixed Cached value if successful, false otherwise
+ * @todo Refactor to use RES_NOTFOUND for return value checks
+ */
+ public function read($key, $conditions = null) {
+ $Memcached =& static::$_Memcached;
+
+ return function($self, $params, $chain) use (&$Memcached) {
+ extract($params);
+ $cachetime = intval($Memcached->get($key . '_expires'));
+ $time = time();
+ return ($cachetime < $time) ? false : $Memcached->get($key);
+ };
+ }
+
+ /**
+ * Delete value from the cache
+ *
+ * @param string $key
+ * @param object $conditions
+ * @return mixed True on successful delete, false otherwise
+ */
+ public function delete($key, $conditions = null) {
+ $Memcached =& static::$_Memcached;
+
+ return function($self, $params, $chain) use (&$Memcached) {
+ extract($params);
+ $Memcached->delete($key . '_expires');
+ return $Memcached->delete($key);
+ };
+ }
+
+ /**
+ * Clears user-space cache
+ *
+ * @return mixed True on successful clear, false otherwise
+ */
+ public function clear() {
+ return static::$_Memcached->flush();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/cache/adapters/Memory.php b/libraries/lithium/storage/cache/adapters/Memory.php
new file mode 100644
index 0000000..9782741
--- /dev/null
+++ b/libraries/lithium/storage/cache/adapters/Memory.php
@@ -0,0 +1,83 @@
+<?php
+
+namespace lithium\storage\cache\adapters;
+
+class Memory extends \lithium\core\Adaptable {
+
+ protected $_cache = array();
+
+ /**
+ * Reads data from $key
+ *
+ * @param string $key
+ * @param mixed $conditions
+ * @return mixed
+ */
+ public function read($key, $conditions = null) {
+ $cache =& $this->_cache;
+
+ return function($self, $params, $chain) use (&$cache) {
+ extract($params);
+ return isset($cache[$key]) ? $cache[$key] : null;
+ };
+ }
+
+ /**
+ * Writes $data to $key
+ *
+ * @param string $key
+ * @param mixed $data
+ * @param mixed $conditions
+ * @return boolean
+ */
+ public function write($key, $data, $conditions = null) {
+ $cache =& $this->_cache;
+
+ return function($self, $params, $chain) use (&$cache) {
+ extract($params);
+ return (bool)($cache[$key] = $data);
+ };
+ }
+
+ /**
+ * Delete a key
+ *
+ * @param string $key
+ * @param mixed $conditions
+ * @return boolean
+ */
+ public function delete($key, $conditions = null) {
+ $cache =& $this->_cache;
+
+ return function($self, $params, $chain) use (&$cache) {
+ extract($params);
+ if (isset($cache[$key])) {
+ unset($cache[$key]);
+ return true;
+ } else {
+ return false;
+ }
+ };
+ }
+
+ /**
+ * Clear all keys
+ *
+ * @return boolean
+ */
+ public function clear() {
+ unset($this->_cache);
+ return true;
+ }
+
+ /**
+ * GC is not enabled for this adapter
+ *
+ * @return boolean
+ */
+ public function clean() {
+ return false;
+ }
+
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/adapters/Cookie.php b/libraries/lithium/storage/session/adapters/Cookie.php
new file mode 100644
index 0000000..28eb945
--- /dev/null
+++ b/libraries/lithium/storage/session/adapters/Cookie.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\session\adapters;
+
+use \lithium\util\Set;
+
+class Cookie extends \lithium\core\Object {
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'name' => '', 'expires' => '+1 day', 'domain' => '',
+ 'path' => '/', 'secure' => false, 'http' => false
+ );
+ parent::__construct((array)$config + $defaults);
+ }
+
+ public function isStarted() {
+ return true;
+ }
+
+ public function isValid() {
+ return true;
+ }
+
+ public function key() {
+ return null;
+ }
+
+ public function read($key, $options = array()) {
+ return function($self, $params, $chain) {
+
+ };
+ }
+
+ public function write($key, $value = null, $options = array()) {
+
+ if (!isset($options['expires']) && $key != $this->_config['name']) {
+ return null;
+ }
+ $config = $this->_config;
+
+ return function($self, $params, $chain) use ($config) {
+ extract($params);
+ $key = is_array($key) ? Set::flatten($key) : array($key => $value);
+ $o = $options + $config;
+
+ 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_unshift($name) . '[' . join('][', $name) . ']');
+ }
+ setcookie(
+ $name, $val, $o['expires'], $o['path'], $o['domain'], $o['secure'], $o['http']
+ );
+ }
+ };
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/adapters/Memory.php b/libraries/lithium/storage/session/adapters/Memory.php
new file mode 100644
index 0000000..2483a11
--- /dev/null
+++ b/libraries/lithium/storage/session/adapters/Memory.php
@@ -0,0 +1,57 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\session\adapters;
+
+/**
+ * Simple memory session storage engine. Used for testing.
+ */
+class Memory extends \lithium\core\Object {
+
+ public $_session = array();
+
+ public function key() {
+ return $_SERVER['UNIQUE_ID'];
+ }
+
+ public function isStarted() {
+ return true;
+ }
+
+ public function check($key, $options = array()) {
+ return isset($this->_session[$key]);
+ }
+
+ public function read($key, $options = array()) {
+ $session = $this->_session;
+
+ return function($self, $params, $chain) use ($session) {
+ extract($params);
+ return isset($session[$key]) ? $session[$key] : null;
+ };
+ }
+
+ public function write($key, $value, $options = array()) {
+ $session =& $this->_session;
+
+ return function($self, $params, $chain) use (&$session) {
+ extract($params);
+ return (bool)($session[$key] = $value);
+ };
+ }
+
+ public function delete($key, $options = array()) {
+ unset($this->_session[$key]);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/storage/session/adapters/Php.php b/libraries/lithium/storage/session/adapters/Php.php
new file mode 100644
index 0000000..fde09e6
--- /dev/null
+++ b/libraries/lithium/storage/session/adapters/Php.php
@@ -0,0 +1,53 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\storage\session\adapters;
+
+class Php extends \lithium\core\Object {
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'name' => '', 'expires' => '+1 day', 'domain' => '',
+ 'path' => '/', 'secure' => false, 'http' => false
+ );
+ parent::__construct((array)$config + $defaults);
+ }
+
+ protected function _init() {
+ if (!isset($_SESSION)) {
+ session_cache_limiter("must-revalidate");
+ session_start();
+ }
+ }
+
+ public function isStarted() {
+ return (isset($_SESSION) && isset($_SESSION['_timestamp']));
+ }
+
+ public function key() {
+ return ($id = session_id()) == '' ? null : $id;
+ }
+
+ public function read($key, $options = array()) {
+ return function($self, $params, $chain) {
+
+ };
+ }
+
+ public static function write($key, $value, $options = array()) {
+ return function($self, $params, $chain) {
+
+ };
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/Helper.php b/libraries/lithium/template/Helper.php
new file mode 100644
index 0000000..0c55a47
--- /dev/null
+++ b/libraries/lithium/template/Helper.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template;
+
+use \lithium\util\String;
+
+abstract class Helper extends \lithium\core\Object {
+
+ /**
+ * Maps helper method names to content types as defined by the `Media` class, where key are
+ * method names, and values are the content type that the method name outputs a link to.
+ *
+ * @var array
+ */
+ public $contentMap = array();
+
+ /**
+ * Holds string templates which will be merged into the rendering context.
+ *
+ * @var array
+ */
+ protected $_strings = array();
+
+ protected $_context = null;
+
+ protected $_autoConfig = array('classes' => 'merge', 'context');
+
+ /**
+ * List of minimized HTML attributes.
+ *
+ * @var array
+ */
+ protected $_minimized = array(
+ 'compact', 'checked', 'declare', 'readonly', 'disabled', 'selected', 'defer', 'ismap',
+ 'nohref', 'noshade', 'nowrap', 'multiple', 'noresize'
+ );
+
+ public function __construct($config = array()) {
+ $defaults = array('handlers' => array(), 'context' => null);
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Imports local string definitions into rendering context.
+ *
+ * @return void
+ */
+ protected function _init() {
+ parent::_init();
+
+ if (!$this->_context) {
+ return;
+ }
+ $this->_context->strings($this->_strings);
+
+ if ($this->_config['handlers']) {
+ $this->_context->handlers($this->_config['handlers']);
+ }
+ }
+
+ /**
+ * Escapes values according to the output type of the rendering context. Helpers that output to
+ * non-HTML/XML contexts should override this method accordingly.
+ *
+ * @param string $value
+ * @return mixed
+ */
+ public function escape($value, $method = null, $options = array()) {
+ $defaults = array('escape' => true);
+ $options += $defaults;
+
+ if ($options['escape'] === false) {
+ return $value;
+ }
+ return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
+ }
+
+ protected function _render($method, $string, $params, $options = array()) {
+ $defaults = array();
+ $options += $defaults;
+
+ foreach ($params as $key => $value) {
+ $params[$key] = $this->_context->applyHandler($this, $method, $key, $value, $options);
+ }
+ $strings = $this->_context ? $this->_context->strings() : $this->_strings;
+ return String::insert(isset($strings[$string]) ? $strings[$string] : $string, $params);
+ }
+
+ protected function _attributes($params, $method = null, $options = array()) {
+ if (!is_array($params)) {
+ return empty($params) ? '' : ' ' . $params;
+ }
+
+ $defaults = array('escape' => true, 'prepend' => ' ', 'append' => '');
+ $options += $defaults;
+ $result = array();
+
+ foreach ($params as $key => $value) {
+ $result[] = $this->_formatAttr($key, $value, $options);
+ }
+ return $result ? $options['prepend'] . implode(' ', $result) . $options['append'] : '';
+ }
+
+ protected function _formatAttr($key, $value, $options = array()) {
+ $defaults = array('escape' => true);
+ $options += $defaults;
+
+ $format = '%s="%s"';
+ $value = (string)$value;
+
+ if (in_array($key, $this->_minimized)) {
+ $isMini = ($value === 1 || $value === true || $value === 'true' || $value == $key);
+ $value = $isMini ? $key : $value;
+ }
+
+ if ($options['escape']) {
+ return sprintf($format, $this->escape($key), $this->escape($value));
+ }
+ return sprintf($format, $key, $value);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/View.php b/libraries/lithium/template/View.php
new file mode 100644
index 0000000..0cccb78
--- /dev/null
+++ b/libraries/lithium/template/View.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template;
+
+use \RuntimeException;
+use \lithium\util\String;
+use \lithium\core\Libraries;
+
+class View extends \lithium\core\Object {
+
+ public $outputFilters = array();
+
+ /**
+ * Holds the details of the current request that originated the call to this view, if
+ * 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
+ */
+ protected $_request = null;
+
+ protected $_loader = null;
+
+ protected $_renderer = null;
+
+ protected $_autoConfig = array('outputFilters' => 'merge', 'request');
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'request' => null,
+ 'vars' => array(),
+ 'loader' => 'File',
+ 'renderer' => 'File',
+ 'outputFilters' => array()
+ );
+ parent::__construct($config + $defaults);
+ }
+
+ protected function _init() {
+ foreach (array('loader', 'renderer') as $key) {
+ if (is_object($this->_config[$key])) {
+ $this->{'_' . $key} = $this->_config[$key];
+ continue;
+ }
+
+ if (!$class = Libraries::locate('adapters.template.view', $this->_config[$key])) {
+ throw new RuntimeException("Template adapter {$this->_config[$key]} not found");
+ }
+ $this->{'_' . $key} = new $class($this->_config);
+ }
+
+ $h = function($data) use (&$h) {
+ return is_array($data) ? array_map($h, $data) : htmlspecialchars((string)$data);
+ };
+ $this->outputFilters += compact('h');
+ }
+
+ public function render($type, $data = array(), $options = array()) {
+ $defaults = array('context' => array());
+ $options += $defaults;
+
+ switch ($type) {
+ case 'all':
+ $content = $this->render('template', $data, $options);
+
+ if (!$options['layout']) {
+ return $content;
+ }
+ $options['context'] += compact('content');
+ return $this->render('layout', $data, $options);
+ case 'template':
+ case 'layout':
+ $template = $this->_loader->template($type, $options);
+ $data = (array)$data + $this->outputFilters;
+ return $this->_renderer->render($template, $data, $options);
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/helpers/Form.php b/libraries/lithium/template/helpers/Form.php
new file mode 100644
index 0000000..ca29d48
--- /dev/null
+++ b/libraries/lithium/template/helpers/Form.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template\helpers;
+
+use \lithium\util\Set;
+
+class Form extends \lithium\template\Helper {
+
+ /**
+ * String templates used by this helper.
+ *
+ * @var array
+ */
+ protected $_strings = array(
+ 'button' => '<input type="{:type}"{:options} />',
+ 'checkbox' => '<input type="checkbox" name="{:name}"{:options} />',
+ 'checkbox-multi' => '<input type="checkbox" name="{:name}[]"{:options} />',
+ 'checkbox-multi-end' => '',
+ 'checkbox-multi-start' => '',
+ 'error' => '<div{:options}>{:content}</div>',
+ 'errors' => '{:content}',
+ 'file' => '<input type="file" name="{:name}"{:options} />',
+ 'form' => '<form {:options}>',
+ 'form-end' => '</form>',
+ 'hidden' => '<input type="hidden" name="{:name}"{:options} />',
+ 'input' => '<div{:wrap}>{:label}{:input}{:error}</div>',
+ 'input-checkbox' => '<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}',
+ 'select-start' => '<select name="{:name}"{:options}>',
+ 'select-multi-start' => '<select name="{:name}[]"{:options}>',
+ 'select-empty' => '<option value=""{:options}> </option>',
+ 'select-option' => '<option value="{:value}"{:options}>{:content}</option>',
+ 'select-end' => '</select>',
+ 'submit' => '<input type="submit"{:options} />',
+ 'submit-image' => '<input type="image" src="{:url}"{:options} />',
+ 'text' => '<input type="text" name="{:name}"{:options} />',
+ 'textarea' => '<textarea name="{:name}"{:options}>{:content}</textarea>',
+ 'fieldset' => '<fieldset{:options}>{:content}</fieldset>',
+ 'fieldset-start' => '<fieldset><legend>{:content}</legend>',
+ 'fieldset-end' => '</fieldset>'
+ );
+
+ /**
+ * Maps method names to template string names, allowing the default template strings to be set
+ * permanently on a per-method basis.
+ *
+ * For example, if all text input fields should be wrapped in `<span />` tags, you can configure
+ * the template string mappings per the following:
+ *
+ * {{{
+ * $this->form->config(array('templates' => array(
+ * 'text' => '<span><input type="text" name="{:name}"{:options} /></span>'
+ * )));
+ * }}}
+ *
+ * Alternatively, you can re-map one type as another. This is useful if, for example, you
+ * include your own helper with custom form template strings which do not match the default
+ * template string names.
+ *
+ * {{{
+ * // Renders all password fields as text fields
+ * $this->form->config(array('templates' => array('password' => 'text')));
+ * }}}
+ *
+ * @var array
+ * @see lithium\template\helpers\Form::config()
+ */
+ protected $_templateMap = array();
+
+ public function __construct($config = array()) {
+ $defaults = array('base' => array(), 'text' => array(), 'textarea' => array());
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Allows you to configure a default set of options which are included on a per-method basis,
+ * and configure method template overrides.
+ *
+ * To force all `<label />` elements to have a default `class` attribute value of "foo"`, simply
+ * do the following:
+ *
+ * {{{
+ * $this->form->config(array('label' => array('class' => 'foo')));
+ * }}}
+ *
+ * @param array $config An associative array where the keys are `Form` method names, 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).
+ * @see lithium\template\helpers\Form::$_templateMap
+ */
+ public function config($config = array()) {
+ if (empty($config)) {
+ return array('templates' => $this->_templateMap) + array_intersect_key(
+ $this->_config, array('base' => '', 'text' => '', 'textarea' => '')
+ );
+ }
+ if (isset($config['templates'])) {
+ $this->_templateMap = $config['templates'] + $this->_templateMap;
+ unset($config['templates']);
+ }
+ return ($this->_config = Set::merge($this->_config, $config)) + array(
+ 'templates' => $this->_templateMap
+ );
+ }
+
+ public function text($name, $options = array()) {
+ list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
+ return $this->_render(__METHOD__, $template, compact('name', 'options'));
+ }
+
+ public function password($name, $options = array()) {
+ list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
+ return $this->_render(__METHOD__, $template, compact('name', 'options'));
+ }
+
+ public function label($name, $title, $options = array()) {
+ list($name, $options, $template) = $this->_defaults(__FUNCTION__, $name, $options);
+ return $this->_render(__METHOD__, $template, compact('name', 'title', 'options'));
+ }
+
+ protected function _defaults($method, $name, $options) {
+ $methodConfig = isset($this->_config[$method]) ? $this->_config[$method] : array();
+ $options += $methodConfig + $this->_config['base'];
+
+ if (isset($options['default'])) {
+ $options['value'] = isset($options['value']) ? $options['value'] : $options['default'];
+ unset($options['default']);
+ }
+ $template = isset($this->_templateMap[$method]) ? $this->_templateMap[$method] : $method;
+ return array($name, $options, $template);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/helpers/Html.php b/libraries/lithium/template/helpers/Html.php
new file mode 100644
index 0000000..e4c3363
--- /dev/null
+++ b/libraries/lithium/template/helpers/Html.php
@@ -0,0 +1,343 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template\helpers;
+
+class Html extends \lithium\template\Helper {
+
+ /**
+ * String templates used by this helper.
+ *
+ * @var array
+ */
+ protected $_strings = array(
+ 'block' => '<div{:options}>{:content}</div>',
+ 'block-end' => '</div>',
+ 'block-start' => '<div{:options}>',
+ 'charset' => '<meta http-equiv="Content-Type" content="{:type}; charset={:charset}" />',
+ 'doctype' => '<!DOCTYPE {:version} PUBLIC "{:dtd}" "{:url}">',
+ 'image' => '<img src="{:path}"{:options} />',
+ 'js-block' => '<script type="text/javascript"{:options}>{:content}</script>',
+ 'js-end' => '</script>',
+ 'js-link' => '<script type="text/javascript" src="{:path}"{:options}></script>',
+ 'js-start' => '<script type="text/javascript"{:options}>',
+ 'link' => '<a href="{:url}"{:options}>{:title}</a>',
+ 'list' => '<ul{:options}>{:content}</ul>',
+ 'list-item' => '<li{:options}>{:content}</li>',
+ 'meta' => '<meta{:options}/>',
+ 'meta-link' => '<link href="{:url}"{:options} />',
+ 'para' => '<p{:options}>{:content}</p>',
+ 'para-start' => '<p{:options}>',
+ 'style' => '<style type="text/css"{:options}>{:content}</style>',
+ 'style-import' => '<style type="text/css"{:options}>@import url({:url});</style>',
+ 'style-link' => '<link rel="{:type}" type="text/css" href="{:path}"{:options} />',
+ 'table-header' => '<th{:options}>{:content}</th>',
+ 'table-header-row' => '<tr{:options}>{:content}</tr>',
+ 'table-cell' => '<td{:options}>{:content}</td>',
+ 'table-row' => '<tr{:options}>{:content}</tr>',
+ 'tag' => '<{:name}{:options}>{:content}</{:name}>',
+ 'tag-end' => '</{:name}>',
+ 'tag-start' => '<{:name}{:options}>'
+ );
+
+ /**
+ * Document type definitions
+ *
+ * @var array
+ */
+ protected $_docTypes = array(
+ 'html4-strict' => array(
+ 'version' => 'HTML',
+ 'dtd' => '-//W3C//DTD HTML 4.01//EN',
+ 'url' => 'http://www.w3.org/TR/html4/strict.dtd'
+ ),
+ 'html4-trans' => array(
+ 'version' => 'HTML',
+ 'dtd' => '-//W3C//DTD HTML 4.01 Transitional//EN',
+ 'url' => 'http://www.w3.org/TR/html4/loose.dtd'
+ ),
+ 'html4-frame' => array(
+ 'version' => 'HTML',
+ 'dtd' => '-//W3C//DTD HTML 4.01 Frameset//EN',
+ 'url' => 'http://www.w3.org/TR/html4/frameset.dtd'
+ ),
+ 'xhtml-strict' => array(
+ 'version' => 'html',
+ 'dtd' => '-//W3C//DTD XHTML 1.0 Strict//EN',
+ 'url' => 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd'
+ ),
+ 'xhtml-trans' => array(
+ 'version' => 'html',
+ 'dtd' => '-//W3C//DTD XHTML 1.0 Transitional//EN',
+ 'url' => 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd'
+ ),
+ 'xhtml-frame' => array(
+ 'version' => 'html',
+ 'dtd' => '-//W3C//DTD XHTML 1.0 Frameset//EN',
+ 'url' => 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd'
+ ),
+ 'xhtml11' => array(
+ 'version' => 'html',
+ 'dtd' => '-//W3C//DTD XHTML 1.1//EN',
+ 'url' => 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd'
+ )
+ );
+
+ /**
+ * Data used for custom <meta /> links.
+ *
+ * @var array
+ */
+ protected $_metaLinks = array(
+ 'atom' => array('type' => 'application/atom+xml', 'rel' => 'alternate'),
+ 'rss' => array('type' => 'application/rss+xml', 'rel' => 'alternate'),
+ 'icon' => array('type' => 'image/x-icon', 'rel' => 'icon')
+ );
+
+ /**
+ * Used by output handlers to calculate asset paths in conjunction with the `Media` class.
+ *
+ * @var array
+ */
+ public $contentMap = array(
+ 'script' => 'js',
+ 'style' => 'css',
+ 'image' => 'image',
+ '_metaLink' => 'generic'
+ );
+
+ /**
+ * Returns a doctype string.
+ *
+ * Possible doctypes:
+ * + html4-strict: HTML4 Strict.
+ * + html4-trans: HTML4 Transitional.
+ * + html4-frame: HTML4 Frameset.
+ * + xhtml-strict: XHTML1 Strict.
+ * + xhtml-trans: XHTML1 Transitional.
+ * + xhtml-frame: XHTML1 Frameset.
+ * + xhtml11: XHTML1.1.
+ *
+ * @param string $type Doctype to use.
+ * @return string Doctype.
+ */
+ public function docType($type = 'xhtml-trans') {
+ if (isset($this->_docTypes[$type])) {
+ return $this->_render(__METHOD__, 'doctype', $this->_docTypes[$type]);
+ }
+ }
+
+ /**
+ * Returns a charset meta-tag.
+ *
+ * @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.
+ */
+ public function charset($charset = null) {
+ $options = array('type' => 'text/html');
+ $options['charset'] = $charset ?: 'utf-8';
+ return $this->_render(__METHOD__, 'charset', $options);
+ }
+
+ /**
+ * Creates an HTML link.
+ *
+ * If $url starts with "http://" this is treated as an external link. Otherwise,
+ * it is treated as a path to controller/action and parsed with the `Html::url()` method.
+ *
+ * If `$url` is empty, `$title` is used instead.
+ *
+ * @param string $title The content to be wrapped by an <a /> tag.
+ * @param mixed $url Lithium-relative URL or array of URL parameters, or external URL
+ * (starts with http://)
+ * @param array $options Array of HTML attributes.
+ * @return string An <a /> element.
+ */
+ public function link($title, $url = null, $options = array()) {
+ $defaults = array('escape' => true);
+ $options += $defaults;
+
+ if (isset($options['type']) && $type = $options['type']) {
+ unset($options['type']);
+ $options = array_diff_key($options, $defaults) + compact('title');
+ return $this->_metaLink($type, $url, $options);
+ }
+
+ $url = is_null($url) ? $title : $url;
+ $params = $options;
+ $options = array_diff_key($options, $defaults);
+ return $this->_render(__METHOD__, 'link', compact('title', 'url', 'options'), $params);
+ }
+
+ /**
+ * Returns a JavaScript include tag (<script /> element). If the filename is prefixed with "/",
+ * the path will be relative to the base path of your application. Otherwise, the path will
+ * be relative to your JavaScript path, usually `webroot/js`.
+ *
+ * @param mixed $path String path to JavaScript file, or an array of paths.
+ * @param array $options
+ * @return string
+ */
+ public function script($path, $options = array()) {
+ $defaults = array('inline' => true);
+ $options += $defaults;
+
+ if (is_array($path)) {
+ $result = join("\n\t", array_map(array(&$this, __FUNCTION__), $path));
+ return ($options['inline']) ? $result . "\n" : null;
+ }
+ $params = compact('path') + array('options' => array_diff_key($options, $defaults));
+
+ $script = $this->_filter(__METHOD__, $params, function($self, $params, $chain) {
+ return $self->invokeMethod('_render', array($chain->method(true), 'js-link', $params));
+ });
+
+ if ($options['inline']) {
+ return $script;
+ }
+ }
+
+ /**
+ * Creates a link element for CSS stylesheets.
+ *
+ * @param mixed $path The name of a CSS style sheet in /app/webroot/css, or an array
+ * containing names of CSS stylesheets in that directory.
+ * @param array $options Array of HTML attributes.
+ * @param boolean $inline If set to false, the generated tag appears in the head tag
+ * of the layout.
+ * @return string CSS <link /> or <style /> tag, depending on the type of link.
+ * @filter
+ */
+ public function style($path, $options = array()) {
+ $defaults = array('type' => 'stylesheet', 'inline' => true);
+ $options += $defaults;
+
+ if (is_array($path)) {
+ $result = join("\n\t", array_map(array(&$this, __FUNCTION__), $path));
+ return ($options['inline']) ? $result . "\n" : null;
+ }
+ $params = compact('path', 'options');
+
+ $filter = function($self, $params, $chain) use ($defaults) {
+ extract($params);
+
+ $type = $options['type'];
+ $options = array_diff_key($options, $defaults);
+ $template = ($type == 'import') ? 'style-import' : 'style-link';
+ $params = compact('type', 'path', 'options');
+
+ return $self->invokeMethod('_render', array($chain->method(true), $template, $params));
+ };
+ return $this->_filter(__METHOD__, $params, $filter);
+ }
+
+ /**
+ * Creates a formatted <img /> element.
+ *
+ * @param string $path Path to the image file, relative to the app/webroot/img/ directory.
+ * @param array $options Array of HTML attributes.
+ * @return string
+ */
+ public function image($path, $options = array()) {
+ $defaults = array('alt' => '');
+ $options += $defaults;
+
+ if (is_array($path)) {
+ $path = $this->_context->url($path);
+ }
+ return $this->_render(__METHOD__, 'image', compact('path', 'options'));
+ }
+
+ /**
+ * Returns a formatted block tag, i.e <div />, <span />, <p />.
+ *
+ * @param string $name Tag name.
+ * @param string $text String content that will appear inside the div element.
+ * If null, only a start tag will be printed
+ * @param array $attributes Additional HTML attributes of the DIV tag
+ * @param boolean $escape If true, $text will be HTML-escaped
+ * @return string The formatted tag element
+ */
+ function tag($name, $content = null, $options = array()) {
+ $options = is_array($options) ? $options : array('class' => $options);
+ return $this->_render(__METHOD__, ($content === null) ? 'tag-start' : 'tag', compact(
+ 'name', 'options', 'content'
+ ));
+ }
+
+ /**
+ * Returns a formatted DIV tag for HTML FORMs.
+ *
+ * @param string $class CSS class name of the div element.
+ * @param string $text String content that will appear inside the div element.
+ * If null, only a start tag will be printed
+ * @param array $attributes Additional HTML attributes of the DIV tag
+ * @param boolean $escape If true, $text will be HTML-escaped
+ * @return string The formatted DIV element
+ */
+ function block($class = null, $content = null, $options = array()) {
+ if ($class) {
+ $options['class'] = $class;
+ }
+ return $this->_render(__METHOD__, 'block', compact('content', 'options'));
+ }
+
+ /**
+ * Returns a formatted P tag.
+ *
+ * @param string $class CSS class name of the p element.
+ * @param string $text String content that will appear inside the p element.
+ * @param array $attributes Additional HTML attributes of the P tag
+ * @param boolean $escape If true, $text will be HTML-escaped
+ * @return string The formatted P element
+ */
+ function para($class, $content, $options = array()) {
+ if ($class) {
+ $options['class'] = $class;
+ }
+ return $this->_render(__METHOD__, ($content === null) ? 'para-start' : 'para', compact(
+ 'content', 'options'
+ ));
+ }
+
+ /**
+ * Creates a link to an external resource.
+ *
+ * @param string $type The title of the external resource
+ * @param mixed $url The address of the external resource or string for content attribute
+ * @param array $attributes Other attributes for the generated tag. If the type attribute
+ * is 'html', 'rss', 'atom', or 'icon', the mime-type is returned.
+ * @return string
+ */
+ protected function _metaLink($type, $url = null, $options = array()) {
+ $options += isset($this->_metaLinks[$type]) ? $this->_metaLinks[$type] : array();
+
+ if ($type == 'icon') {
+ $url = $url ?: 'favicon.ico';
+ $standard = $this->_render(__METHOD__, 'meta-link', compact('url', 'options'), array(
+ 'handlers' => array('url' => 'path')
+ ));
+ $options['rel'] = 'shortcut icon';
+
+ $ieFix = $this->_render(__METHOD__, 'meta-link', compact('url', 'options'), array(
+ 'handlers' => array('url' => 'path')
+ ));
+ return "{$standard}\n\t{$ieFix}";
+ }
+
+ return $this->_render(__METHOD__, 'meta-link', compact('url', 'options'), array(
+ 'handlers' => array()
+ ));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/view/Renderer.php b/libraries/lithium/template/view/Renderer.php
new file mode 100644
index 0000000..c50278e
--- /dev/null
+++ b/libraries/lithium/template/view/Renderer.php
@@ -0,0 +1,302 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+abstract class Renderer extends \lithium\core\Object {
+
+ /**
+ * These configuration variables will automatically be assigned to their corresponding protected
+ * properties when the object is initialized.
+ *
+ * @var array
+ */
+ protected $_autoConfig = array('request', 'context', 'strings', 'handlers');
+
+ /**
+ * Context values that exist across all templates rendered in this context. These values
+ * are usually rendered in the layout template after all other values have rendered.
+ *
+ * @var array
+ */
+ protected $_context = array(
+ 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array()
+ );
+
+ /**
+ * `Renderer`'s dependencies. These classes are used by the output handlers to generate URLs
+ * for dynamic resources and static assets.
+ *
+ * @var array
+ * @see Renderer::$_handlers
+ */
+ protected $_classes = array('router' => 'lithium\http\Router', 'media' => 'lithium\http\Media');
+
+ /**
+ * Contains the list of helpers currently in use by this rendering context. Helpers are loaded
+ * via the `helper()` method, which is called by `Renderer::__get()`, allowing for on-demand
+ * loading of helpers.
+ *
+ * @var array
+ */
+ protected $_helpers = array();
+
+ /**
+ * Aggregates named string templates used by helpers. Can be overridden to change the default
+ * strings a helper uses.
+ *
+ * @var array
+ */
+ protected $_strings = array();
+
+ protected $_request = 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
+ * method that triggered the handler, and the array of options passed to the `_render()`. These
+ * handlers are shared among all helper objects, and are automatically triggered whenever a
+ * helper method renders a template string (using `_render()`) and a key which is to be embedded
+ * in the template string matches an array key of a corresponding handler.
+ *
+ * @var array
+ * @see lithium\template\view\Renderer::applyHandler()
+ * @see lithium\template\view\Renderer::handlers()
+ */
+ protected $_handlers = array();
+
+ public function __construct($config = array()) {
+ $defaults = array(
+ 'strings' => array(), 'handlers' => array(), 'request' => null, 'context' => array(
+ 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array()
+ )
+ );
+ parent::__construct((array)$config + $defaults);
+ }
+
+ /**
+ * Sets the default output handlers for string template inputs.
+ *
+ * @return void
+ */
+ protected function _init() {
+ parent::_init();
+
+ $request =& $this->_request;
+ $context =& $this->_context;
+ $classes =& $this->_classes;
+ $self =& $this;
+
+ $this->_handlers += array(
+ 'url' => function($url) use (&$classes, &$self, &$request) {
+ return $classes['router']::match($url ?: '', $request);
+ },
+ 'path' => function($path, $ref, $options = array()) use (&$self, &$classes, &$request) {
+ $defaults = array('base' => $request ? $request->env('base') : '');
+ 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";
+ }
+ );
+ }
+
+ public function __get($property) {
+ $context = $this->_context;
+ $helpers = $this->_helpers;
+
+ $filter = function($self, $params, $chain) use ($context, $helpers) {
+ $property = $params['property'];
+
+ foreach (array('context', 'helpers') as $key) {
+ if (isset(${$key}[$property])) {
+ return ${$key}[$property];
+ }
+ }
+ return $self->helper($property);
+ };
+ return $this->_filter(__METHOD__, compact('property'), $filter);
+ }
+
+ /**
+ * Dispatches method calls for (a) rendering context values or (b) applying handlers to pieces
+ * of content. If `$method` is a key in `Renderer::$_context`, the corresponding context value
+ * will be returned (with the value run through a matching handler if one is available). If
+ * `$method` is a key in `Renderer::$_handlers`, the value passed as the first parameter in the
+ * method call will be passed through the handler and returned.
+ *
+ * @param string $method The method name to call, usually either a rendering context value or a
+ * content handler.
+ * @param array $params
+ * @return mixed
+ * @see lithium\template\view\Renderer::$_context
+ * @see lithium\template\view\Renderer::$_handlers
+ * @see lithium\template\view\Renderer::applyHandler()
+ */
+ public function __call($method, $params) {
+ if (!isset($this->_context[$method]) && !isset($this->_handlers[$method])) {
+ return isset($params[0]) ? $params[0] : null;
+ }
+ if (!isset($this->_handlers[$method]) && empty($params)) {
+ return $this->_context[$method];
+ }
+ if (isset($this->_context[$method]) && !empty($params)) {
+ if (is_array($this->_context[$method])) {
+ $this->_context[$method][] = $params[0];
+ } else {
+ $this->_context[$method] = $params[0];
+ }
+ }
+ if (!isset($this->_context[$method])) {
+ $params += array(null, array());
+ return $this->applyHandler(null, null, $method, $params[0], $params[1]);
+ }
+ return $this->applyHandler(null, null, $method, $this->_context[$method]);
+ }
+
+ /**
+ * Brokers access to helpers attached to this rendering context, and loads helpers on-demand if
+ * they are not available.
+ *
+ * @param string $name Helper name
+ * @param array $config
+ * @return object
+ */
+ public function helper($name, $config = array()) {
+ if ($class = Libraries::locate('helpers', ucfirst($name))) {
+ return $this->_helpers[$name] = new $class($config + array('context' => $this));
+ }
+ throw new RuntimeException("Helper $name not found");
+ }
+
+ /**
+ * Manages template strings.
+ *
+ * @param mixed $strings
+ * @return mixed
+ */
+ public function strings($strings = null) {
+ if (is_array($strings)) {
+ return $this->_strings += $strings;
+ }
+ if (is_string($strings)) {
+ return isset($this->_strings[$strings]) ? $this->_strings[$strings] : null;
+ }
+ return $this->_strings;
+ }
+
+ /**
+ * Returns either one or all context values for this rendering context. Context values persist
+ * across all templates rendered in the current context, and are usually outputted in a layout
+ * template.
+ *
+ * @param string $property If unspecified, an associative array of all context values is
+ * returned. If a string is specified, the context value matching the name given
+ * will be returned, or `null` if that name does not exist.
+ * @return mixed A string or array, depending on whether `$property` is specified.
+ * @see lithium\template\view\Renderer::$_context
+ */
+ public function context($property = null) {
+ if (!empty($property)) {
+ return isset($this->_context[$property]) ? $this->_context[$property] : null;
+ }
+ return $this->_context;
+ }
+
+ /**
+ * Gets or adds content handlers from/to this rendering context, depending on the value of
+ * `$handlers`. For more on how to implement handlers and the various types, see
+ * `applyHandler()`.
+ *
+ * @param mixed $handlers If `$handlers` is empty or no value is provided, the current list
+ * of handlers is returned. If `$handlers` is a string, the handler with the name
+ * matching the string will be returned, or null if one does not exist. If
+ * `$handlers` is an array, the handlers named in the array will be merged into
+ * the list of handlers in this rendering context, with the pre-existing handlers
+ * taking precedence over those newly added.
+ * @return mixed Returns an array of handlers or a single handler reference, depending on the
+ * value of `$handlers`.
+ * @see lithium\template\view\Renderer::applyHandler()
+ * @see lithium\template\view\Renderer::$_handlers
+ */
+ public function handlers($handlers = null) {
+ if (is_array($handlers)) {
+ return $this->_handlers += $handlers;
+ }
+ if (is_string($handlers)) {
+ return isset($this->_handlers[$handlers]) ? $this->_handlers[$handlers] : null;
+ }
+ return $this->_handlers;
+ }
+
+ /**
+ * Filters a piece of content through a content handler. A handler can be:
+ * - a string containing the name of a method defined in `$helper`. The method is called with 3
+ * parameters: the value to be handled, the helper method called (`$method`) and the
+ * `$options` that were passed into `applyHandler`.
+ * - an array where the first element is an object reference, and the second element is a method
+ * name. The method name given will be called on the object with the same parameters as
+ * above.
+ * - a closure, which takes the value as the first parameter, an array containing an instance of
+ * the calling helper and the calling method name as the second, and `$options` as the third.
+ * In all cases, handlers should return the transformed version of `$value`.
+ *
+ * @param object $helper The instance of the object (usually a helper) that is invoking
+ * @param string $method The object (helper) method which is applying the handler to the content
+ * @param string $name The name of the value to which the handler is applied, i.e. `'url'`,
+ * `'path'` or `'title'`.
+ * @param mixed $value The value to be transformed by the handler, which is ultimately returned.
+ * @param array $options Any options which should be passed to the handler used in this call.
+ * @return mixed The transformed value of `$value`, after it has been processed by a handler.
+ * @see lithium\template\view\Renderer::handlers()
+ * @see lithium\template\view\Renderer::$_handlers
+ */
+ public function applyHandler($helper, $method, $name, $value, $options = array()) {
+ if (!(isset($this->_handlers[$name]) && $handler = $this->_handlers[$name])) {
+ return $value;
+ }
+
+ switch (true) {
+ case is_string($handler) && is_object($helper):
+ return $helper->invokeMethod($handler, array($value, $method, $options));
+ case is_array($handler) && is_object($handler[0]):
+ list($object, $func) = $handler;
+ return $object->invokeMethod($func, array($value, $method, $options));
+ case is_callable($handler):
+ return $handler($value, array($helper, $method), $options);
+ default:
+ return $value;
+ }
+ }
+
+ /**
+ * Returns the `Request` object associated with this rendering context.
+ *
+ * @return object Returns an instance of `lithium\action\Request`, which provides the context for
+ * URLs, etc. which are generated in any templates rendered by this context.
+ */
+ public function request() {
+ return $this->_request;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/view/Stream.php b/libraries/lithium/template/view/Stream.php
new file mode 100644
index 0000000..fd0d620
--- /dev/null
+++ b/libraries/lithium/template/view/Stream.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template\view;
+
+/**
+ * Stream wrapper implementation based on the example provided at
+ * http://us3.php.net/manual/en/stream.streamwrapper.example-1.php, and inspired by the work of
+ * Paul M. Jones (http://paul-m-jones.com/) and Mike Naberezny (http://mikenaberezny.com/).
+ *
+ * Enables pure PHP template files to auto-escape output and implement custom content filtering.
+ */
+class Stream {
+
+ protected $_position = 0;
+
+ protected $_stats = array();
+
+ protected $_data = null;
+
+ protected $_path = null;
+
+ function stream_open($path, $mode, $options, &$opened_path) {
+ $path = str_replace('lithium.template://', '', $path);
+
+ if (empty($path)) {
+ return false;
+ }
+
+ $success = ($this->_data = file_get_contents($path));
+ $this->_stats = stat($path);
+
+ if ($success === false) {
+ return false;
+ }
+
+ $echo = '/\<\?=\s*([^(?:;\s*\?>)(?:\s*\?>)]+)\s*;?\s*\?>/';
+ $this->_data = preg_replace('/\<\?=@/', '<?php echo ', $this->_data);
+ $this->_data = preg_replace($echo, '<?php echo $h($1); ?>', $this->_data);
+ return true;
+ }
+
+ function stream_read($count) {
+ $result = substr($this->_data, $this->_position, $count);
+ $this->_position += strlen($result);
+ return $result;
+ }
+
+ function stream_tell() {
+ return $this->_position;
+ }
+
+ function stream_eof() {
+ return ($this->_position >= strlen($this->_data));
+ }
+
+ function stream_seek($offset, $whence) {
+ switch ($whence) {
+ case SEEK_SET:
+ if ($offset < strlen($this->_data) && $offset >= 0) {
+ $this->_position = $offset;
+ return true;
+ }
+ return false;
+ case SEEK_CUR:
+ if ($offset >= 0) {
+ $this->_position += $offset;
+ return true;
+ }
+ return false;
+ case SEEK_END:
+ if (strlen($this->_data) + $offset >= 0) {
+ $this->_position = strlen($this->_data) + $offset;
+ return true;
+ }
+ return false;
+ default:
+ }
+ return false;
+ }
+
+ public function stream_stat() {
+ return $this->_stats;
+ }
+
+ public function url_stat() {
+ return $this->_stats;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/template/view/adapters/File.php b/libraries/lithium/template/view/adapters/File.php
new file mode 100644
index 0000000..34ac10f
--- /dev/null
+++ b/libraries/lithium/template/view/adapters/File.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\template\view\adapters;
+
+use \Exception;
+use \lithium\util\String;
+use \lithium\core\Libraries;
+
+/**
+ * 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. <?=).
+ *
+ * For more information about implementing your own template loaders or renderers, see the
+ * `lithium\template\View` class.
+ *
+ * @package lithium.template.view.adapters
+ * @see lithium\template\View
+ * @see lithium\template\view\Stream
+ */
+class File extends \lithium\template\view\Renderer {
+
+ protected $_autoConfig = array(
+ 'classes' => 'merge', 'request', 'context', 'strings', 'handlers'
+ );
+
+ protected $_classes = array(
+ 'stream' => '\lithium\template\view\Stream',
+ 'router' => 'lithium\http\Router',
+ 'media' => 'lithium\http\Media'
+ );
+
+ public function __construct($config = array()) {
+ $defaults = array('protocol' => 'lithium.template', 'classes' => array());
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Renders content from a template file provided by `template()`.
+ *
+ * @param string $template
+ * @param string $data
+ * @param string $context
+ * @return string
+ */
+ public function render($template, $data = array(), $options = array()) {
+ $this->_context += $options['context'];
+
+ $__t__ = $this->_config['protocol'] . '://' . $template;
+ unset($options);
+ extract($data, EXTR_OVERWRITE);
+ ob_start();
+
+ include $__t__;
+ return $this->_context['content'] = ob_get_clean();
+ }
+
+ /**
+ * Returns a template file name
+ *
+ * @param string $type
+ * @param string $options
+ * @return void
+ * @todo Replace me with include_path search and move to File adapter
+ */
+ public function template($type, $options) {
+ if (!isset($this->_config[$type])) {
+ return null;
+ }
+ $options = array_filter($options, function($item) { return is_string($item); });
+
+ if (isset($options['plugin'])) {
+ $options['library'] = $options['plugin'];
+ }
+
+ $options['library'] = isset($options['library']) ? $options['library'] : 'app';
+ $library = Libraries::get($options['library']);
+ $options['library'] = $library['path'];
+
+ foreach ((array)$this->_config[$type] as $path) {
+ if (file_exists($path = String::insert($path, $options))) {
+ return $path;
+ }
+ }
+ throw new Exception("Template not found at {$path}");
+ }
+
+ protected function _init() {
+ parent::_init();
+
+ if (!in_array($this->_config['protocol'], stream_get_wrappers())) {
+ stream_wrapper_register($this->_config['protocol'], $this->_classes['stream']);
+ }
+ }
+}
diff --git a/libraries/lithium/template/view/adapters/Simple.php b/libraries/lithium/template/view/adapters/Simple.php
new file mode 100644
index 0000000..486cb73
--- /dev/null
+++ b/libraries/lithium/template/view/adapters/Simple.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace lithium\template\view\adapters;
+
+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
+ * simple templates (no conditionals or looping) or testing.
+ *
+ * @package lithium.template.view.adapters
+ */
+class Simple extends \lithium\template\view\Renderer {
+
+ protected $_classes = array();
+
+ public function __construct($config = array()) {
+ $defaults = array('classes' => array());
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Renders content from a template file provided by `template()`.
+ *
+ * @param string $template
+ * @param array $data
+ * @param array $context
+ * @param array $options
+ * @return string
+ */
+ public function render($template, $data = array(), $context = array(), $options = array()) {
+ foreach ($data as $key => $val) {
+ switch (true) {
+ case is_object($val):
+ try {
+ $data[$key] = (string)$val;
+ } catch (Exception $e) {
+ $data[$key] = '';
+ }
+ break;
+ case is_array($val):
+ $data = array_merge($data, Set::flatten($val));
+ break;
+ }
+ }
+ return String::insert($template, $data, $options);
+ }
+
+ /**
+ * Returns a template string
+ *
+ * @param string $type
+ * @param array $options
+ * @return string
+ */
+ public function template($type, $options) {
+ return isset($options[$type]) ? $options[$type] : '';
+ }
+}
diff --git a/libraries/lithium/test/Dispatcher.php b/libraries/lithium/test/Dispatcher.php
new file mode 100644
index 0000000..fa85c22
--- /dev/null
+++ b/libraries/lithium/test/Dispatcher.php
@@ -0,0 +1,177 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Dispatcher extends \lithium\core\StaticObject {
+
+ protected static $_classes = array(
+ 'group' => '\lithium\test\Group'
+ );
+
+ public static function run($group = null, $options = array()) {
+ $default = array(
+ 'base' => null,
+ 'case' => null,
+ 'group' => null,
+ 'filters' => array(),
+ 'path' => LITHIUM_LIBRARY_PATH . '/lithium/tests/cases',
+ );
+ $options += $default;
+ $group = $group ?: static::_group($options);
+
+ if (!$group) {
+ return null;
+ }
+ $title = $options['case'] ?: '\lithium\tests\cases'. $options['group'];
+ list($results, $filters) = static::_execute($group, Set::normalize($options['filters']));
+
+ if (is_null($options['base'])) {
+ $options['base'] = $options['path'];
+ }
+
+ return compact('title', 'results', 'filters');
+ }
+
+ public static function menu($type, $data = null, $parent = null) {
+ if ($data == null) {
+ $tests = function($callback, $path, $base = null) {
+ $name = str_replace($base .'/', '', basename($path, '.php'));
+ if (is_dir($path)) {
+ $paths = glob($path . '/*');
+ if (!empty($paths)) {
+ $results = array();
+ $count = count($paths);
+ $results[$name] = array_filter(array_map(
+ $callback,
+ array_pad(array(), $count, $callback),
+ $paths,
+ array_pad(array(), $count, $path)
+ ));
+ if ($base == null) {
+ return $results[$name];
+ }
+ return $results;
+ }
+ }
+ return $name;
+ };
+ $data = $tests($tests, LITHIUM_LIBRARY_PATH . '/lithium/tests/cases');
+ }
+ $result = null;
+
+ $format = function($test) use ($type) {
+ if ($type == 'html') {
+ if ($test == 'group') {
+ return '<li><a href="?group=%1$s">%2$s</a><ul>%3$s</ul></li>';
+ }
+ return '<li><a href="?case=\lithium\tests\cases%2$s\%1$s">%1$s</a></li>';
+ }
+
+ if ($type == 'txt') {
+ if ($test == 'group') {
+ return "-group %1$s\n%2$s\n";
+ }
+ return "-case %1$s\n";
+ }
+ };
+
+ if (is_array($data)) {
+ foreach ($data as $key => $row) {
+ if (is_array($row)) {
+ if (is_string($key)) {
+ $key = strtolower($key);
+ $parent = $parent . '\\' . $key;
+ $result = sprintf($format('group'), $parent, $key, static::menu($type, $row, $parent));
+ } else {
+ $result .= static::menu($type, $row, $parent);
+ }
+ } else {
+ $result .= sprintf($format('case'), $row, $parent);
+ }
+ }
+ }
+ if ($parent == null) {
+ if ($type == 'html') {
+ return sprintf('<ul>%s</ul>', $result);
+ }
+ if ($type == 'txt') {
+ return sprintf("\n%s\n", $result);
+ }
+ }
+ return $result;
+ }
+
+ public static function process($results) {
+ return array_reduce((array)$results, function($stats, $result) {
+ $stats = (array)$stats + array(
+ 'asserts' => 0,
+ 'passes' => array(),
+ 'fails' => array(),
+ 'exceptions' => array(),
+ 'errors' => array()
+ );
+ $result = empty($result[0]) ? array($result) : $result;
+
+ foreach ($result as $response) {
+ if (empty($response['result'])) {
+ continue;
+ }
+ $result = $response['result'];
+
+ if (in_array($result, array('fail', 'exception'))) {
+ $stats['errors'][] = $response;
+ }
+ unset($response['file'], $response['result']);
+
+ if (in_array($result, array('pass', 'fail'))) {
+ $stats['asserts']++;
+ }
+ if (in_array($result, array('pass', 'fail', 'exception'))) {
+ $stats[Inflector::pluralize($result)][] = $response;
+ }
+ }
+ return $stats;
+ });
+ }
+
+ protected static function _group($options) {
+ if (!empty($options['case'])) {
+ return new static::$_classes['group'](array('items' => array(new $options['case'])));
+ } elseif (isset($options['group'])) {
+ return new static::$_classes['group'](array('items' => (array)$options['group']));
+ }
+ }
+
+ protected static function _execute($group, $filters) {
+ $tests = $group->tests();
+ $filterResults = array();
+
+ foreach ($filters as $filter => $options) {
+ $options = isset($options['apply']) ? $options['apply'] : array();
+ $tests = $filter::apply($tests, $options);
+ }
+ $results = $tests->run();
+
+ foreach ($filters as $filter => $options) {
+ $options = isset($options['analyze']) ? $options['analyze'] : array();
+ $filterResults[$filter] = $filter::analyze($results, $options);
+ }
+ return array($results, $filterResults);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/Group.php b/libraries/lithium/test/Group.php
new file mode 100644
index 0000000..7bfe547
--- /dev/null
+++ b/libraries/lithium/test/Group.php
@@ -0,0 +1,79 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\Inflector;
+use \lithium\util\Collection;
+
+class Group extends \lithium\util\Collection {
+
+ protected function _init() {
+ parent::_init();
+
+ $items = $this->_items;
+ $this->_items = array();
+
+ foreach ($items as $item) {
+ $this->add($item);
+ }
+ }
+
+ public static function all($options = array()) {
+ $defaults = array('transform' => false, 'library' => true);
+ $options += $defaults;
+
+ $m = '/\\\\tests\\\\cases\\\\(.+)Test$/';
+ $filter = function($class) use ($m) { return preg_replace($m, '\\\\\1', $class); };
+ $classes = Libraries::find($options['library'], array(
+ 'filter' => '/\w+Test$/', 'recursive' => true
+ ));
+ return $options['transform'] ? array_map($filter, $classes) : $classes;
+ }
+
+ public function add($test = null, $options = array()) {
+ $callback = function($test) {
+ if (empty($test)) {
+ return array();
+ }
+
+ if (is_object($test) && $test instanceof \lithium\test\Unit) {
+ $test = get_class($test);
+ } elseif (is_string($test) && $test[0] == '\\') {
+ $test = Libraries::find(true, array(
+ 'recursive' => true,
+ 'path' => '/tests/cases' . str_replace("\\", "/", $test)
+ ));
+ } elseif (is_string($test)) {
+ $test = array("lithium\\tests\cases\\$test");
+ }
+ return (array)$test;
+ };
+
+ if (is_array($test)) {
+ foreach ($test as $t) {
+ $this->_items = array_filter(array_merge($this->_items, $callback($t)));
+ }
+ return $this->_items;
+ }
+ return $this->_items = array_merge($this->_items, $callback($test));
+ }
+
+ public function tests($params = array(), $options = array()) {
+ $tests = new Collection();
+ array_map(function($test) use ($tests) { $tests[] = new $test; }, $this->_items);
+ return $tests;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/Reporter.php b/libraries/lithium/test/Reporter.php
new file mode 100644
index 0000000..ae42b87
--- /dev/null
+++ b/libraries/lithium/test/Reporter.php
@@ -0,0 +1,19 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test;
+
+class Reporter extends \lithium\core\Object {
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/Unit.php b/libraries/lithium/test/Unit.php
new file mode 100644
index 0000000..132048c
--- /dev/null
+++ b/libraries/lithium/test/Unit.php
@@ -0,0 +1,618 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test;
+
+use \Exception;
+use \lithium\util\Set;
+use \lithium\util\String;
+use \lithium\util\Validator;
+use \lithium\util\audit\Debugger;
+use \lithium\util\reflection\Inspector;
+
+class Unit extends \lithium\core\Object {
+
+ protected $_results = array();
+
+ protected $_reporter = null;
+
+ protected $_expected = array();
+
+ /**
+ * 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($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) {
+ if (preg_match('/^Skipped test/', $e->getMessage())) {
+ $this->_result('skip', array());
+ }
+ $this->_handleException($e, __LINE__ - 5);
+ return;
+ }
+ set_error_handler($options['handler']);
+
+ foreach ($methods as $method) {
+ $this->_runTestMethod($method, $options);
+ }
+
+ restore_error_handler();
+ return $this->_results;
+ }
+
+ /**
+ * Returns the class name that is the subject under test for this test case.
+ *
+ * @return string
+ * @todo This clearly needs refactoring to remove $map
+ */
+ public function subject() {
+ $map = array('lithium\tests\cases' => 'lithium', 'app\tests\cases' => 'app');
+ $class = str_replace(array_keys($map), array_values($map), get_class($this));
+ return preg_replace('/Test$/', '', $class);
+ }
+
+ public function methods() {
+ static $methods;
+ return $methods ?: $methods = array_values(preg_grep('/^test/', get_class_methods($this)));
+ }
+
+ public function setUp() {
+ }
+
+ public function tearDown() {
+ }
+
+ public function skip() {
+ }
+
+ 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));
+ }
+
+ public function assert($expression, $message = '{:message}', $data = array()) {
+ $trace = Debugger::trace(array('start' => 1, 'format' => 'array'));
+ $methods = $this->methods();
+ $i = 1;
+
+ while ($i < count($trace)) {
+ if (in_array($trace[$i]['function'], $methods) && $trace[$i - 1]['object'] == $this) {
+ break;
+ }
+ $i++;
+ }
+
+ if (strpos($message, "{:message}") !== false) {
+ $data['message'] = $this->_message($data);
+ }
+
+ $result = 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);
+ return $expression;
+ }
+
+ public function assertEqual($expected, $result, $message = '{:message}') {
+ $data = null;
+ if ($expected != $result) {
+ $data = $this->_compare('equal', $expected, $result);
+ }
+ $this->assert($expected == $result, $message, $data);
+ }
+
+ public function assertNotEqual($expected, $result, $message = '{:message}') {
+ $this->assert($result != $expected, $message, compact('expected', 'result'));
+ }
+
+ public function assertIdentical($expected, $result, $message = '{:message}') {
+ if ($expected !== $result) {
+ $data = $this->_compare('identical', $expected, $result);
+ }
+ $this->assert($expected === $result, $message);
+ }
+
+ public function assertTrue($result, $message = '{:message}') {
+ $expected = true;
+ $this->assert(!empty($result), $message, compact('expected', 'result'));
+ }
+
+ public function assertFalse($result, $message = '{:message}') {
+ $expected = false;
+ $this->assert(empty($result), $message, compact('expected', 'result'));
+ }
+
+ public function assertNull($result, $message = '{:message}') {
+ $expected = null;
+ $this->assert($result === null, $message, compact('expected', 'result'));
+ }
+
+ public function assertNoPattern($expected, $result, $message = '{:message}') {
+ $this->assert(!preg_match($expected, $result), $message, compact('expected', 'result'));
+ }
+
+ public function assertPattern($expected, $result, $message = '{:message}') {
+ $this->assert(!!preg_match($expected, $result), $message, compact('expected', 'result'));
+ }
+
+ /**
+ * Takes an array $expected and generates a regex from it to match the provided $string.
+ * Samples for $expected:
+ *
+ * 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',
+ * '/p',
+ * array('p' => true),
+ * '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.
+ *
+ * @param string $string An HTML/XHTML/XML string
+ * @param array $expected An array, see above
+ * @param string $message SimpleTest failure output string
+ * @access public
+ */
+ function assertTags($string, $expected, $fullDebug = false) {
+ $regex = array();
+ $normalized = array();
+
+ foreach ((array) $expected as $key => $val) {
+ if (!is_numeric($key)) {
+ $normalized[] = array($key => $val);
+ } else {
+ $normalized[] = $val;
+ }
+ }
+ $i = 0;
+
+ foreach ($normalized as $tags) {
+ $i++;
+ if (is_string($tags) && $tags{0} == '<') {
+ $tags = array(substr($tags, 1) => array());
+ } elseif (is_string($tags)) {
+ $tagsTrimmed = preg_replace('/\s+/m', '', $tags);
+
+ if (preg_match('/^\*?\//', $tags, $match) && $tagsTrimmed !== '//') {
+ $prefix = array(null, null);
+
+ if ($match[0] == '*/') {
+ $prefix = array('Anything, ', '.*?');
+ }
+ $regex[] = array(
+ sprintf('%sClose %s tag', $prefix[0], substr($tags, strlen($match[0]))),
+ sprintf('%s<[\s]*\/[\s]*%s[\s]*>[\n\r]*', $prefix[1], substr(
+ $tags, strlen($match[0])
+ )),
+ $i
+ );
+ continue;
+ }
+
+ if (!empty($tags) && preg_match('/^preg\:\/(.+)\/$/i', $tags, $matches)) {
+ $tags = $matches[1];
+ $type = 'Regex matches';
+ } else {
+ $tags = preg_quote($tags, '/');
+ $type = 'Text equals';
+ }
+ $regex[] = array(sprintf('%s "%s"', $type, $tags), $tags, $i);
+ continue;
+ }
+ foreach ($tags as $tag => $attributes) {
+ $regex[] = array(
+ sprintf('Open %s tag', $tag),
+ sprintf('[\s]*<%s', preg_quote($tag, '/')),
+ $i
+ );
+ if ($attributes === true) {
+ $attributes = array();
+ }
+ $attrs = array();
+ $explanations = array();
+
+ foreach ($attributes as $attr => $val) {
+ if (is_numeric($attr) && preg_match('/^preg\:\/(.+)\/$/i', $val, $matches)) {
+ $attrs[] = $matches[1];
+ $explanations[] = sprintf('Regex "%s" matches', $matches[1]);
+ continue;
+ } else {
+ $quotes = '"';
+
+ if (is_numeric($attr)) {
+ $attr = $val;
+ $val = '.+?';
+ $explanations[] = sprintf('Attribute "%s" present', $attr);
+ } elseif (!empty($val) && preg_match('/^preg\:\/(.+)\/$/i', $val, $matches)) {
+ $quotes = '"?';
+ $val = $matches[1];
+ $explanations[] = sprintf('Attribute "%s" matches "%s"', $attr, $val);
+ } else {
+ $explanations[] = sprintf('Attribute "%s" == "%s"', $attr, $val);
+ $val = preg_quote($val, '/');
+ }
+ $attrs[] = '[\s]+' . preg_quote($attr, '/') . "={$quotes}{$val}{$quotes}";
+ }
+ }
+ if ($attrs) {
+ $permutations = $this->_arrayPermute($attrs);
+ $permutationTokens = array();
+ foreach ($permutations as $permutation) {
+ $permutationTokens[] = join('', $permutation);
+ }
+ $regex[] = array(
+ sprintf('%s', join(', ', $explanations)),
+ $permutationTokens,
+ $i
+ );
+ }
+ $regex[] = array(sprintf('End %s tag', $tag), '[\s]*\/?[\s]*>[\n\r]*', $i);
+ }
+ }
+
+ foreach ($regex as $i => $assertation) {
+ list($description, $expressions, $itemNum) = $assertation;
+ $matches = false;
+
+ foreach ((array)$expressions as $expression) {
+ if (preg_match(sprintf('/^%s/s', $expression), $string, $match)) {
+ $matches = true;
+ $string = substr($string, strlen($match[0]));
+ break;
+ }
+ }
+
+ if (!$matches) {
+ $this->assert(false, sprintf(
+ '{:message} - Item #%d / regex #%d failed: %s', $itemNum, $i, $description
+ ));
+ // if ($fullDebug) {
+ // debug($string, true);
+ // debug($regex, true);
+ // }
+ return false;
+ }
+ }
+ return $this->assert(true, '%s');
+ }
+
+ /**
+ * 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.
+ *
+ * @param mixed $message A string indicating what the error text is expected to be. This can
+ * be an exact string, a /-delimited regular expression, or true, indicating that
+ * any error text is acceptable.
+ * @return void
+ */
+ public function expectException($message = true) {
+ $this->_expected[] = $message;
+ }
+
+ /**
+ * Reports test result messages.
+ *
+ * @param string $type The type of result being reported. Can be `'pass'`, `'fail'`, `'skip'`
+ * or `'exception'`.
+ * @param array $info An array of information about the test result. At a minimum, this should
+ * contain a `'message'` key. Other possible keys are `'file'`, `'line'`,
+ * `'class'`, `'method'`, `'assertion'` and `'data'`.
+ * @param array $options Currently unimplemented.
+ * @return void
+ */
+ protected function _result($type, $info, $options = array()) {
+ $info = (array('result' => $type) + $info);
+ $defaults = array();
+ $options += $defaults;
+
+ if ($this->_reporter) {
+ $filtered = $this->_reporter->__invoke($info);
+ $info = is_array($filtered) ? $filtered : $info;
+ }
+ $this->_results[] = $info;
+ }
+
+ /**
+ * Runs an individual test method, collecting results and catching exceptions along the way.
+ *
+ * @param string $method The name of the test method to run.
+ * @param array $options
+ * @return void
+ */
+ protected function _runTestMethod($method, $options) {
+ $this->setUp();
+ $params = compact('options', 'method');
+
+ $this->_filter(__CLASS__ . '::run', $params, function($self, $params, $chain) {
+ try {
+ $method = $params['method'];
+ $lineFlag = __LINE__ + 1;
+ $self->$method();
+ } catch (Exception $e) {
+ if (preg_match('/^Skipped test/', $e->getMessage())) {
+ $self->invokeMethod('_result', array('skip', array(
+ 'message' => $e->getMessage()
+ )));
+ } else {
+ $self->invokeMethod('_handleException', array($e, $lineFlag));
+ }
+ }
+ });
+ $this->tearDown();
+ }
+
+ /**
+ * Normalizes `Exception` objects and PHP error data into a single array format, and checks
+ * each error against the list of expected errors (set using `expectException()`). If a match
+ * 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.
+ *
+ * @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
+ * of variables relevant to the scope of where the error occurred).
+ * @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) {
+ if (is_object($exception)) {
+ $data = array();
+
+ foreach (array('message', 'file', 'line', 'trace') as $key) {
+ $method = 'get' . ucfirst($key);
+ $data[$key] = $exception->{$method}();
+ }
+ $ref = $exception->getTrace();
+ $ref = $ref[0] + array('class' => null);
+
+ 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);
+ }
+
+ /**
+ * 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
+ * added to the reporting stack contained in `Unit::$_results`.
+ * @return void
+ * @todo Refactor so that reporters handle trace formatting.
+ */
+ protected function _reportException($exception, $lineFlag = null) {
+ $initFrame = current($exception['trace']) + array('class' => '-', 'function' => '-');
+ foreach ($exception['trace'] as $frame) {
+ if (isset($scopedFrame)) {
+ break;
+ }
+ if (isset($frame['class']) && in_array($frame['class'], Inspector::parents($this))) {
+ $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,
+ 'format' => '{:functionRef}, line {:line}',
+ 'includeScope' => false,
+ 'scope' => array_filter(array(
+ 'functionRef' => __NAMESPACE__ . '\{closure}',
+ 'line' => $lineFlag
+ )),
+ ))
+ ));
+ }
+
+ /**
+ * Compare the expected with the result. If `$result` is null `$expected` equals `$type`
+ * and `$result` equals `$expected`.
+ *
+ * @param string $type The type of comparison either `'identical'` or `'equal'` (default).
+ * @param mixed $expected The expected value.
+ * @param mixed $result An optional result value, defaults to `null`
+ * @param string $trace An optional trace used internally to track arrays and objects,
+ * defaults to `null`.
+ * @return array Data with the keys `trace'`, `'expected'` and `'result'`.
+ */
+ protected function _compare($type, $expected, $result = null, $trace = null) {
+ if ($result == null) {
+ $result = $expected;
+ $expected = $type;
+ $type = 'equal';
+ }
+ $types = array(
+ 'trace' => $trace, 'expected' => gettype($expected), 'result' => gettype($result)
+ );
+ if ($types['expected'] !== $types['result']) {
+ return $types;
+ }
+
+ $data = array();
+ $isObject = false;
+
+ if (is_object($expected)) {
+ $isObject = true;
+ $expected = (array)$expected;
+ $result = (array)$result;
+ }
+
+ if (is_array($expected)) {
+ foreach ($expected as $key => $value) {
+ $check = array_key_exists($key, $result) ? $result[$key] : false;
+ $newTrace = (($isObject == true) ? "{$trace}->{$key}" : "{$trace}[{$key}]");
+
+ if ($type === 'identical') {
+ if ($value === $check) {
+ continue;
+ }
+ if ($check === false) {
+ $trace = $newTrace;
+ return compact('trace', 'expected', 'result');
+ }
+ } else {
+ if ($value == $check) {
+ continue;
+ }
+ if (!is_array($value)) {
+ $trace = $newTrace;
+ return compact('trace', 'expected', 'result');
+ }
+ }
+ $compare = $this->_compare($type, $value, $check, $newTrace);
+
+ if ($compare !== true) {
+ $data[] = $compare;
+ }
+ }
+ return $data;
+ }
+
+ if ($type === 'identical') {
+ if ($expected === $result) {
+ return true;
+ }
+ } else {
+ if ($expected == $result) {
+ return true;
+ }
+ }
+ $data = compact('trace', 'expected', 'result');
+ return $data;
+ }
+
+ /**
+ * 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()
+ */
+ protected function _message($data = array()) {
+ $messages = null;
+ if (!empty($data[0])) {
+ foreach ($data as $message) {
+ $messages .= $this->_message($message);
+ }
+ return $messages;
+ }
+
+ $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)
+ );
+ }
+
+ /**
+ * Generates all permutation of an array $items and returns them in a new array.
+ *
+ * @param array $items An array of items
+ * @return array
+ */
+ protected function _arrayPermute($items, $perms = array()) {
+ static $permuted;
+
+ if (empty($perms)) {
+ $permuted = array();
+ }
+
+ if (empty($items)) {
+ $permuted[] = $perms;
+ } else {
+ $numItems = count($items) - 1;
+
+ for ($i = $numItems; $i >= 0; --$i) {
+ $newItems = $items;
+ $newPerms = $perms;
+ list($tmp) = array_splice($newItems, $i, 1);
+ array_unshift($newPerms, $tmp);
+ $this->_arrayPermute($newItems, $newPerms);
+ }
+ return $permuted;
+ }
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/test/filters/Coverage.php b/libraries/lithium/test/filters/Coverage.php
new file mode 100644
index 0000000..7fe0f57
--- /dev/null
+++ b/libraries/lithium/test/filters/Coverage.php
@@ -0,0 +1,291 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test\filters;
+
+use \lithium\core\Libraries;
+use \lithium\util\String;
+use \lithium\util\Collection;
+use \lithium\util\reflection\Inspector;
+
+/**
+ * Runs code coverage analysis for the executed tests.
+ */
+class Coverage extends \lithium\core\StaticObject {
+
+ /**
+ * Collects coverage analysis results from
+ *
+ */
+ protected static $_results = array();
+
+ /**
+ * Takes an instance of an object (usually a Collection object) containing unit test case
+ * instances. Attaches code coverage filtering to test cases.
+ *
+ * @param object $object Instance of Collection containing instances of lithium\test\Unit
+ * @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
+ * method are:
+ * -'method': The name of method to attach to, defaults to 'run'.
+ * @return object Returns the instance of $object with code coverage analysis triggers applied.
+ * @see lithium\test\filters\Coverage::collect()
+ */
+ public static function apply($object, $options = array()) {
+ $defaults = array('method' => 'run');
+ $options += $defaults;
+ $m = $options['method'];
+
+ $object->invoke('applyFilter', array($m, function($self, $params, $chain) use ($options) {
+ xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
+ $chain->next($self, $params, $chain);
+ $results = xdebug_get_code_coverage();
+ xdebug_stop_code_coverage();
+ Coverage::collect($self->subject(), $results, $options);
+ }));
+ return $object;
+ }
+
+ /**
+ * Collects code coverage analysis results from `xdebug_get_code_coverage()`.
+ *
+ * @param string $class Class name that these test results correspond to.
+ * @param array $results A results array from `xdebug_get_code_coverage()`.
+ * @param array $options Set of options defining how results should be collected.
+ * @return void
+ * @see lithium\test\Coverage::analyze()
+ * @todo Implement $options['merging']
+ */
+ public static function collect($class, $results, $options = array()) {
+ $defaults = array('merging' => 'class');
+ $options += $defaults;
+
+ foreach ($results as $file => $lines) {
+ unset($results[$file][0]);
+ }
+
+ switch ($options['merging']) {
+ case 'class':
+ default:
+ if (!isset(static::$_results[$class])) {
+ static::$_results[$class] = array();
+ }
+ static::$_results[$class][] = $results;
+ break;
+ }
+ }
+
+ /**
+ * Analyzes code coverage results collected from XDebug, and performs coverage density analysis.
+ *
+ * @param array $classes Optional. A list of classes to analyze coverage on. By default, gets
+ * all defined subclasses of lithium\test\Unit which are currently in memory.
+ * @return array Returns an array indexed by file and line, showing the number of instances
+ * each line was called.
+ */
+ public static function analyze($results, $classes = array()) {
+ $classes = $classes ?: array_filter(get_declared_classes(), function($class) {
+ return (!is_subclass_of($class, 'lithium\test\Unit'));
+ });
+ $classes = array_values(array_intersect((array)$classes, array_keys(static::$_results)));
+ $densities = $result = array();
+
+ foreach ($classes as $class) {
+ $classMap = array($class => Libraries::path($class));
+ $densities += static::_density(static::$_results[$class], $classMap);
+ }
+ $executableLines = array();
+
+ if (!empty($classes)) {
+ $executableLines = array_combine($classes, array_map(
+ function($cls) { return Inspector::executable($cls, array('public' => false)); },
+ $classes
+ ));
+ }
+
+ foreach ($densities as $class => $density) {
+ $executable = $executableLines[$class];
+ $covered = array_intersect(array_keys($density), $executable);
+ $uncovered = array_diff($executable, $covered);
+ $percentage = round(count($covered) / (count($executable) ?: 1), 4) * 100;
+ $result[$class] = compact('class', 'executable', 'covered', 'uncovered', 'percentage');
+ }
+ return $result;
+ }
+
+ /**
+ * Reduces the results of multiple XDebug code coverage runs into a single 2D array of the
+ * aggregate line coverage density per file.
+ *
+ * @param array $results An array containing multiple runs of raw XDebug coverage data, where
+ * each array key is a file name, and it's value is XDebug's coverage
+ * data for that file.
+ * @param array $classMap An optional map with class names as array keys and corresponding file
+ * names as values. Used to filter the returned results, and will cause the array
+ * keys of the results to be class names instead of file names.
+ * @return array
+ */
+ protected static function _density($runs, $classMap = array()) {
+ $results = array();
+
+ foreach ($runs as $run) {
+ foreach ($run as $file => $coverage) {
+ if (!empty($classMap)) {
+ if (!$class = array_search($file, $classMap)) {
+ continue;
+ }
+ $file = $class;
+ }
+ if (!isset($results[$file])) {
+ $results[$file] = array();
+ }
+ $coverage = array_filter($coverage, function($line) { return ($line === 1); });
+
+ foreach ($coverage as $line => $isCovered) {
+ if (!isset($results[$file][$line])) {
+ $results[$file][$line] = 0;
+ }
+ $results[$file][$line]++;
+ }
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * Outputs the coverage analysis to a specific format
+ *
+ * @param string $format [required] html,txt
+ * @param array $data [required] from Coverage::analysis()
+ * @return string
+ */
+ public static function output($format, $analysis) {
+ if (empty($analysis)) {
+ return null;
+ }
+ $output = null;
+ $aggregate = array('covered' => 0, 'executable' => 0);
+
+ foreach ($analysis 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']);
+
+ $uncovered = array_flip($coverage['uncovered']);
+ $contents = explode("\n", file_get_contents($file));
+ array_unshift($contents, ' ');
+ $count = count($contents);
+
+ for ($i = 1; $i <= $count; $i++) {
+ if (isset($uncovered[$i])) {
+ if (!isset($out[$i - 2])) {
+ $out[$i - 2] = array(
+ 'class' => 'ignored',
+ 'data' => '...'
+ );
+ }
+ if (!isset($out[$i - 1])) {
+ $out[$i - 1] = array(
+ 'class' => 'covered',
+ 'data' => $contents[$i - 1]
+ );
+ }
+ $out[$i] = array(
+ 'class' => 'uncovered',
+ 'data' => $contents[$i]
+ );
+
+ if (!isset($uncovered[$i + 1])) {
+ $out[$i + 1] = array(
+ 'class' => 'covered',
+ 'data' => $contents[$i + 1]
+ );
+ }
+ } elseif (isset($out[$i - 1]) && $out[$i - 1]['data'] !== '...' && !isset($out[$i]) && !isset($out[$i + 1])) {
+ $out[$i] = array(
+ 'class' => 'ignored',
+ 'data' => '...'
+ );
+ }
+ }
+ $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'));
+ }
+ }
+ return $output;
+ }
+
+ /**
+ * Returns header for stats
+ *
+ * @param string $format [required] html,txt
+ * @param string $class [required] class name
+ * @param array $data [required] from output
+ * @return string
+ */
+ public static function stats($format, $class, $data) {
+ $covered = count($data['covered']) . ' of ' . count($data['executable']);
+
+ if ($format == 'html') {
+ $title = "{$class}: {$covered} lines covered (<em>{$data['percentage']}%</em>)";
+ return '<h4 class="coverage">' . $title . '</h4>';
+ }
+ }
+
+ /**
+ * Returns row or file in specified format
+ *
+ * @param string $format [required] html,txt
+ * @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'])
+ )
+ );
+ }
+
+ if ($format === 'txt') {
+ 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/filters/Profiler.php b/libraries/lithium/test/filters/Profiler.php
new file mode 100644
index 0000000..8b789a0
--- /dev/null
+++ b/libraries/lithium/test/filters/Profiler.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\test\filters;
+
+class Profiler extends \lithium\core\StaticObject {
+
+ /**
+ * Collects profiling results from test-wrapping filters.
+ */
+ protected static $_results = array();
+
+ /**
+ * Maps class names to test class names
+ */
+ protected static $_classMap = array();
+
+ /**
+ * Contains the list of profiler checks to run against each test. Values can be string
+ * function names, arrays containing function names as the first key and function parameters
+ * as subsequent keys, or closures.
+ *
+ * @var array
+ * @see lithium\test\Profiler::check()
+ */
+ protected static $_metrics = array(
+ 'Time' => array(
+ 'function' => array('microtime', true),
+ 'format' => 'seconds'
+ ),
+ 'Current Memory' => array(
+ 'function' => 'memory_get_usage',
+ 'format' => 'bytes'
+ ),
+ 'Peak Memory' => array(
+ 'function' => 'memory_get_peak_usage',
+ 'format' => 'bytes'
+ ),
+ 'Current Memory (Xdebug)' => array(
+ 'function' => 'xdebug_memory_usage',
+ 'format' => 'bytes'
+ ),
+ 'Peak Memory (Xdebug)' => array(
+ 'function' => 'xdebug_peak_memory_usage',
+ 'format' => 'bytes'
+ )
+ );
+
+ protected static $_formatters = array();
+
+ /**
+ * Verifies that the corresponding function exists for each built-in profiler check.
+ * Initializes display formatters.
+ *
+ * @return void
+ */
+ public static function __init() {
+ foreach (static::$_metrics as $name => $check) {
+ $function = current((array)$check['function']);
+
+ if (is_string($check['function']) && !function_exists($check['function'])) {
+ unset(static::$_metrics[$name]);
+ }
+ }
+
+ static::$_formatters = array(
+ 'seconds' => function($value) { return number_format($value, 4) . 's'; },
+ 'bytes' => function($value) { return number_format($value / 1024, 3) . 'k'; }
+ );
+ }
+
+ /**
+ * Add, remove, or modify a profiler check.
+ *
+ * @param mixed $name
+ * @param string $value
+ * @see lithium\test\Profiler::$_metrics
+ * @return mixed
+ */
+ public function check($name, $value = null) {
+ if (is_null($value) && !is_array($name)) {
+ return isset(static::$_metrics[$name]) ? static::$_metrics[$name] : null;
+ }
+
+ if ($value === false) {
+ unset(static::$_metrics[$name]);
+ return;
+ }
+
+ if (!empty($value)) {
+ static::$_metrics[$name] = $value;
+ }
+
+ if (is_array($name)) {
+ static::$_metrics = $name + static::$_metrics;
+ }
+ }
+
+ public static function apply($object, $options = array()) {
+ $defaults = array('method' => 'run', 'checks' => static::$_metrics);
+ $options += $defaults;
+ $m = $options['method'];
+
+ $object->invoke('applyFilter', array($m, function($self, $params, $chain) use ($options) {
+ $start = $results = array();
+
+ $runCheck = function($check) {
+ switch (true) {
+ case (is_object($check) || is_string($check)):
+ return $check();
+ break;
+ case (is_array($check)):
+ $function = array_shift($check);
+ $result = !$check ? $check() : call_user_func_array($function, $check);
+ break;
+ }
+ return $result;
+ };
+
+ foreach ($options['checks'] as $name => $check) {
+ $start[$name] = $runCheck($check['function']);
+ }
+ $methodResult = $chain->next($self, $params, $chain);
+
+ foreach ($options['checks'] as $name => $check) {
+ $results[$name] = $runCheck($check['function']) - $start[$name];
+ }
+ Profiler::collect($self->subject(), $params['method'], $results, $options + array(
+ 'test' => get_class($self)
+ ));
+ return $methodResult;
+ }));
+ return $object;
+ }
+
+ public static function collect($class, $method, $results, $options = array()) {
+ $defaults = array('test' => null);
+ $options += $defaults;
+
+ static::$_classMap[$options['test']] = $class;
+ static::$_results[$class][$method] = $results;
+ }
+
+ public static function analyze($results, $classes = array()) {
+ $metrics = array();
+
+ foreach ($results as $testCase) {
+ foreach ($testCase as $assertion) {
+ if ($assertion['result'] != 'pass' && $assertion['result'] != 'fail') {
+ continue;
+ }
+ $class = static::$_classMap[$assertion['class']];
+
+ if (!isset($metrics[$class])) {
+ $metrics[$class] = array('assertions' => 0);
+ }
+ $metrics[$class]['assertions']++;
+ }
+ }
+
+ foreach (static::$_results as $class => $methods) {
+ foreach ($methods as $methodName => $timers) {
+ foreach ($timers as $title => $value) {
+ if (!isset($metrics[$class][$title])) {
+ $metrics[$class][$title] = 0;
+ }
+ $metrics[$class][$title] += $value;
+ }
+ }
+ }
+ return $metrics;
+ }
+
+ public static function output($format, $data) {
+ $totals = array();
+
+ foreach ($data as $class => $metrics) {
+ foreach ($metrics as $title => $value) {
+ $totals[$title] = isset($totals[$title]) ? $totals[$title] : 0;
+ $totals[$title]+= $value;
+ }
+ }
+ echo '<h3>Benchmarks</h3>';
+ echo '<table class="metrics"><tbody>';
+
+ foreach ($totals as $title => $value) {
+ if (!isset(static::$_metrics[$title])) {
+ continue;
+ }
+ $formatter = static::$_formatters[static::$_metrics[$title]['format']];
+ echo '<tr>';
+ echo '<td class="metric-name">' . $title . '</th>';
+ echo '<td class="metric">' . $formatter($value) . '</td>';
+ echo '</tr>';
+ }
+ echo '</tbody></table>';
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/action/ControllerTest.php b/libraries/lithium/tests/cases/action/ControllerTest.php
new file mode 100644
index 0000000..6b8c9c4
--- /dev/null
+++ b/libraries/lithium/tests/cases/action/ControllerTest.php
@@ -0,0 +1,259 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\Controller;
+
+class TestMediaClass extends \lithium\http\Media {
+
+ public static function render(&$response, $data = null, $options = array()) {
+ $response->options = $options;
+ $response->data = $data;
+ }
+}
+
+class PostsController extends Controller {
+
+ public $stopped = false;
+
+ public function index($test = false) {
+ if ($test) {
+ return array('foo' => 'bar');
+ }
+ return 'List of posts';
+ }
+
+ public function delete($id = null) {
+ if (empty($id)) {
+ return $this->redirect('/posts', array('exit' => false));
+ }
+ return "Deleted {$id}";
+ }
+
+ public function send() {
+ $this->redirect('/posts');
+ }
+
+ public function view($id = null) {
+ if (!empty($id)) {
+ // throw new NotFoundException();
+ }
+ $this->render(array('text', 'data' => 'This is a post'));
+ }
+
+ public function view2($id = null) {
+ $this->render('view');
+ }
+
+ public function view3($id = null) {
+ $this->render(array('layout' => false, 'template' => 'view'));
+ }
+
+ protected function _safe() {
+ throw new Exception('Something wrong happened');
+ }
+
+ public function access($var) {
+ return $this->{$var};
+ }
+
+ protected function _stop() {
+ $this->stopped = true;
+ }
+}
+
+class ControllerRequest extends \lithium\action\Request {
+}
+
+class ControllerResponse extends \lithium\action\Response {
+
+ public $hasRendered = false;
+
+ public function render() {
+ $this->hasRendered = true;
+ }
+}
+
+class ControllerTest extends \lithium\test\Unit {
+
+ /**
+ * Tests that render settings and dynamic class dependencies can properly be injected into the
+ * controller through the constructor.
+ *
+ * @return void
+ */
+ public function testConstructionWithCustomProperties() {
+ $postsController = new PostsController();
+
+ $result = $postsController->access('_render');
+ $this->assertIdentical($result['layout'], 'default');
+
+ $result = $postsController->access('_classes');
+ $this->assertIdentical($result['response'], '\lithium\action\Response');
+
+ $postsController = new PostsController(array(
+ 'render' => array('layout' => false),
+ 'classes' => array('response' => '\app\extensions\http\Response')
+ ));
+
+ $result = $postsController->access('_render');
+ $this->assertIdentical($result['layout'], false);
+
+ $result = $postsController->access('_classes');
+ $this->assertIdentical($result['response'], '\app\extensions\http\Response');
+ }
+
+ /**
+ * Tests that controllers can be instantiated with custom request objects.
+ *
+ * @return void
+ */
+ public function testConstructionWithCustomRequest() {
+ $request = new ControllerRequest();
+ $postsController = new PostsController(compact('request'));
+ $result = get_class($postsController->request);
+ $this->assertEqual($result, 'lithium\tests\cases\action\ControllerRequest');
+ }
+
+ /**
+ * Tests the use of `Controller::__invoke()` for dispatching requests to action methods. Also
+ * tests that using PHP's callable syntax yields the same result as calling `__invoke()`
+ * explicitly.
+ *
+ * @return void
+ */
+ public function testMethodInvokation() {
+ $postsController = new PostsController();
+ $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);
+
+ $result2 = $postsController(null, array('action' => 'index', 'args' => array()));
+ $this->assertEqual($result2, $result);
+
+ $postsController = new PostsController();
+ $this->expectException('/Template not found/');
+ $result = $postsController->__invoke(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');
+ $this->assertEqual($result->headers, $headers);
+
+ $result = $postsController->access('_render');
+ $this->assertEqual($result['data'], array('foo' => 'bar'));
+
+ $postsController = new PostsController();
+ $result = $postsController(null, array('action' => 'view', 'args' => array('2')));
+
+ $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');
+ $this->assertEqual($result->headers(), $headers);
+
+ $result = $postsController->access('_render');
+ $this->assertEqual($result['data'], array('This is a post'));
+ }
+
+ /**
+ * Tests that calls to `Controller::redirect()` correctly write redirect headers to the
+ * response object.
+ *
+ * @return void
+ */
+ public function testRedirectResponse() {
+ $postsController = new PostsController();
+
+ $result = $postsController->__invoke(null, array('action' => 'delete'));
+ $this->assertEqual($result->body(), '');
+
+ $headers = array('Location' => '/posts');
+ $this->assertEqual($result->headers, $headers);
+
+ $postsController = new PostsController();
+ $result = $postsController(null, array('action' => 'delete', 'args' => array('5')));
+
+ $this->assertEqual($result->body(), 'Deleted 5');
+ $this->assertFalse($postsController->stopped);
+
+ $postsController = new PostsController(array('classes' => array(
+ 'response' => __NAMESPACE__ . '\ControllerResponse'
+ )));
+ $this->assertFalse($postsController->stopped);
+
+ $postsController->__invoke(null, array('action' => 'send'));
+ $this->assertTrue($postsController->stopped);
+
+ $result = $postsController->access('_render');
+ $this->assertTrue($result['hasRendered']);
+
+ $this->assertEqual($postsController->response->body(), null);
+ $this->assertEqual(
+ $postsController->response->headers,
+ array('Location' => '/posts')
+ );
+ }
+
+ /**
+ * Tests calling `Controller::render()` with parameters to render an alternate template from
+ * the default.
+ *
+ * @return void
+ */
+ public function testRenderWithAlternateTemplate() {
+ $postsController = new PostsController(array('classes' => array(
+ 'media' => __NAMESPACE__ . '\TestMediaClass'
+ )));
+
+ $result = $postsController(null, array('action' => 'view2'));
+ $this->assertEqual('view', $result->options['template']);
+ $this->assertEqual('default', $result->options['layout']);
+
+ $result = $postsController(null, array('action' => 'view3'));
+ $this->assertEqual('view', $result->options['template']);
+ $this->assertFalse($result->options['layout']);
+ }
+
+ /**
+ * Verifies that protected methods (i.e. prefixed with '_'), and methods declared in the
+ * Controller base class cannot be accessed.
+ *
+ * @return void
+ */
+ public function testProtectedMethodAccessAttempt() {
+ $postsController = new PostsController();
+ $this->expectException('/^Private/');
+ $result = $postsController->__invoke(null, array('action' => 'redirect'));
+
+ $this->assertEqual($result->body, null);
+ $this->assertEqual($result->headers(), array());
+
+ $postsController = new PostsController();
+ $this->expectException('/^Private/');
+ $result = $postsController->invoke('_safe');
+
+ $this->assertEqual($result->body, null);
+ $this->assertEqual($result->headers(), array());
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/action/RequestTest.php b/libraries/lithium/tests/cases/action/RequestTest.php
new file mode 100644
index 0000000..1c21267
--- /dev/null
+++ b/libraries/lithium/tests/cases/action/RequestTest.php
@@ -0,0 +1,54 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class RequestTest extends \lithium\test\Unit {
+
+ public $request = null;
+
+ public function setUp() {
+ $this->request = new Request(array('init' => false));
+ }
+
+ public function testFilesNormalization() {
+ $result = $this->request->normalizeFiles(array(
+ 'fileA' => array(
+ 'name' => 'fileA.txt', 'type' => 'text/plain',
+ 'tmp_name' => '', 'error' => 4, 'size' => 0
+ ),
+ 'fileB' => array(
+ 'name' => array(
+ 'a' => 'fileBa', 'b' => array('c' => ''), 'd' => array('e' => array('f' => ''))
+ ),
+ 'type' => array(
+ 'a' => '', 'b' => array('c' => ''), 'd' => array('e' => array('f' => ''))
+ ),
+ 'tmp_name' => array(
+ 'a' => '', 'b' => array('c' => ''), 'd' => array('e' => array('f' => ''))
+ ),
+ 'error' => array(
+ 'a' => 4, 'b' => array('c' => 4), 'd' => array('e' => array('f' => 4))
+ ),
+ 'size' => array(
+ 'a' => 0, 'b' => array('c' => 0), 'd' => array('e' => array('f' => 0)),
+ )
+ )
+ ));
+
+ // print_r($result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/action/ResponseTest.php b/libraries/lithium/tests/cases/action/ResponseTest.php
new file mode 100644
index 0000000..a3da804
--- /dev/null
+++ b/libraries/lithium/tests/cases/action/ResponseTest.php
@@ -0,0 +1,150 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class TestRequestType extends \lithium\action\Request {
+
+ public function type() {
+ return 'foo';
+ }
+}
+
+class MockResponse extends Response {
+
+ public $testHeaders = array();
+
+ public function render() {
+ $this->testHeaders = array();
+ parent::render();
+ $this->headers = array();
+ }
+
+ protected function _writeHeader($header, $code = null) {
+ $this->testHeaders[] = $header;
+ }
+}
+
+class ResponseTest extends \lithium\test\Unit {
+
+ public $response = null;
+
+ public function setUp() {
+ $this->response = new MockResponse(array('init' => false));
+ }
+
+ public function testDefaultTypeInitialization() {
+ $this->response = new Response(array('request' => new TestRequestType()));
+ $this->assertEqual('foo', $this->response->type());
+ }
+
+ public function testTypeManipulation() {
+ $this->assertEqual('text/html', $this->response->type());
+ $this->assertEqual('html', $this->response->type('html'));
+ $this->assertEqual('json', $this->response->type('json'));
+ $this->assertEqual('json', $this->response->type());
+ }
+
+ public function testResponseRendering() {
+ $this->response->body = 'Document body';
+
+ ob_start();
+ $this->response->render();
+ $result = ob_get_clean();
+ $this->assertEqual('Document body', $result);
+ $this->assertEqual(array('HTTP/1.1 200 OK'), $this->response->testHeaders);
+
+ ob_start();
+ echo $this->response;
+ $result = ob_get_clean();
+ $this->assertEqual('Document body', $result);
+ $this->assertEqual(array('HTTP/1.1 200 OK'), $this->response->testHeaders);
+
+ $this->response->body = 'Created';
+ $this->response->status(201);
+ $this->response->disableCache();
+
+ ob_start();
+ $this->response->render();
+ $result = ob_get_clean();
+ $this->assertEqual('Created', $result);
+
+ $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'
+ ),
+ 'Pragma: no-cache'
+ );
+ $this->assertEqual($headers, $this->response->testHeaders);
+ }
+
+ /**
+ * Tests various methods of specifying HTTP status codes.
+ *
+ * @return void
+ */
+ public function testStatusCodes() {
+ $this->response->status('Created');
+ ob_start();
+ $this->response->render();
+ $result = ob_get_clean();
+ $this->assertEqual(array('HTTP/1.1 201 Created'), $this->response->testHeaders);
+
+ $this->response->status('See Other');
+ ob_start();
+ $this->response->render();
+ $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);
+ }
+
+ /**
+ * Tests location headers and custom header add-ons, like 'download'.
+ *
+ * @return void
+ */
+ public function testHeaderTypes() {
+ $this->response->headers('download', 'report.csv');
+ ob_start();
+ $this->response->render();
+ ob_end_clean();
+
+ $headers = array(
+ 'HTTP/1.1 200 OK',
+ 'Content-Disposition: attachment; filename="report.csv"'
+ );
+ $this->assertEqual($headers, $this->response->testHeaders);
+
+ $this->response = new MockResponse();
+ $this->response->headers('location', '/');
+ ob_start();
+ $this->response->render();
+ ob_end_clean();
+
+ $headers = array('HTTP/1.1 302 Found', 'Location: /');
+ $this->assertEqual($headers, $this->response->testHeaders);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/CommandTest.php b/libraries/lithium/tests/cases/console/CommandTest.php
new file mode 100644
index 0000000..a467e18
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/CommandTest.php
@@ -0,0 +1,223 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class TestCommandForCommandTest extends \lithium\console\Command {
+
+ public $case = null;
+
+ protected $_dontShow = null;
+
+ protected $_classes = array(
+ 'response' => '\lithium\tests\cases\console\TestResponseForCommandTest'
+ );
+
+ public function testRun() {
+ return 'test run';
+ }
+}
+
+class TestResponseForCommandTest extends \lithium\console\Response {
+
+ public function __construct($config = array()) {
+ parent::__construct($config);
+ $this->output = null;
+ $this->error = null;
+ }
+
+ public function output($string) {
+ return $this->output .= $string;
+ }
+
+ public function error($string) {
+ return $this->error .= $string;
+ }
+
+ public function __destruct() {
+ $this->output = null;
+ $this->error = null;
+ }
+}
+
+class CommandTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->request = new Request(array('input' => fopen('php://temp', 'w+')));
+
+ $this->working = LITHIUM_APP_PATH;
+ if (!empty($_SERVER['PWD'])) {
+ $this->working = $_SERVER['PWD'];
+ }
+ }
+
+ public function testConstruct() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = array('working' => $this->working);
+ $result = $command->request->env;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testInvoke() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = 'test run';
+ $result = $command('testRun');
+ $this->assertEqual($expected, $result);
+
+ $command->request->params['named'] = array(
+ 'case' => 'lithium.tests.cases.console.CommandTest'
+ );
+ $expected = 'test run';
+ $command('testRun');
+ $this->assertTrue(!empty($command->case));
+ }
+
+ public function testOut() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "ok\n";
+ $result = $command->out('ok');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testOutArray() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+
+ $expected = "line 1\nline 2\n";
+ $command->out(array('line 1', 'line 2'));
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testErr() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "ok\n";
+ $result = $command->err('ok');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testErrArray() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "line 1\nline 2\n";
+ $command->err(array('line 1', 'line 2'));
+ $result = $command->response->error;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testNl() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "\n\n\n";
+ $result = $command->nl(3);
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHr() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "----\n";
+ $command->hr(4);
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHeader() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "----\nheader\n----\n";
+ $command->header('header', 4);
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testColumns() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = "data1\t\ndata2\t\n";
+ $command->columns(array('col1' => 'data1', 'col2' => 'data2'));
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHelp() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = preg_quote("usage: lithium TestCommandForCommandTest [--case=val]\n\n");
+ $command->help();
+ $result = $command->response->output;
+ $this->assertPattern("/^{$expected}/", $result);
+ }
+
+ public function testRun() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ $expected = array(
+ '--------------------------------------------------------------------------------',
+ 'Available Commands',
+ '--------------------------------------------------------------------------------',
+ ''
+ );
+ $command->run();
+ $result = explode("\n", $command->response->output);
+
+ $this->assertEqual($expected[0], $result[0]);
+ $this->assertEqual($expected[1], $result[1]);
+ $this->assertEqual($expected[2], $result[2]);
+ $this->assertEqual(end($expected), end($result));
+
+ for ($i = 3; $i < count($result) - 1; $i++) {
+ $this->assertPattern('/^\s-\s[A-Za-z0-9\\_]+$/', $result[$i]);
+ }
+ }
+
+ public function testIn() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ fwrite($command->request->input, 'nada mucho');
+ rewind($command->request->input);
+
+ $expected = "nada mucho";
+ $result = $command->in('What up dog?');
+ $this->assertEqual($expected, $result);
+
+ $expected = "What up dog? \n > ";
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+
+ }
+
+ public function testInWithDefaultOption() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ fwrite($command->request->input, ' ');
+ rewind($command->request->input);
+
+ $expected = "y";
+ $result = $command->in('What up dog?', array('default' => 'y'));
+ $this->assertEqual($expected, $result);
+
+ $expected = "What up dog? \n [y] > ";
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+
+ }
+
+ public function testInWithOptions() {
+ $command = new TestCommandForCommandTest(array('request' => $this->request));
+ fwrite($command->request->input, 'y');
+ rewind($command->request->input);
+
+ $expected = "y";
+ $result = $command->in('Everything Cool?', array('choices' => array('y', 'n')));
+ $this->assertEqual($expected, $result);
+
+ $expected = "Everything Cool? (y/n) \n > ";
+ $result = $command->response->output;
+ $this->assertEqual($expected, $result);
+
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/DispatcherTest.php b/libraries/lithium/tests/cases/console/DispatcherTest.php
new file mode 100644
index 0000000..6e71396
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/DispatcherTest.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class TestCommandForDispatcherTest extends \lithium\console\Command {
+
+ public function run($param = null) {
+ return 'test run' . $param;
+ }
+
+ public function testAction() {
+ return 'test action';
+ }
+}
+
+class TestRequestForDispatcherTest extends \lithium\console\Request {
+
+ public $params = array(
+ 'command' => '\lithium\tests\cases\console\TestCommandForDispatcherTest'
+ );
+}
+
+class DispatcherTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->server = $_SERVER;
+ $_SERVER['argv'] = array();
+ }
+
+ public function tearDown() {
+ $_SERVER = $this->server;
+ }
+
+ public function testEmptyConfigReturnRules() {
+ $result = Dispatcher::config();
+ $expected = array('rules' => array());
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testConfigWithClasses() {
+ Dispatcher::config(array(
+ 'classes' => array(
+ 'request' => '\lithium\tests\cases\console\TestRequestForDispatcherTest'
+ )
+ ));
+ $expected = 'test run';
+ $result = Dispatcher::run();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRunWithCommand() {
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array(
+ '\lithium\tests\cases\console\TestCommandForDispatcherTest'
+ )
+ )));
+ $expected = 'test run';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRunWithPassed() {
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array(
+ '\lithium\tests\cases\console\TestCommandForDispatcherTest',
+ ' with param'
+ )
+ )));
+ $expected = 'test run with param';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRunWithAction() {
+ $result = Dispatcher::run(new Request(array(
+ 'args' => array(
+ '\lithium\tests\cases\console\TestCommandForDispatcherTest',
+ 'testAction'
+ )
+ )));
+ $expected = 'test action';
+ $this->assertEqual($expected, $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
new file mode 100644
index 0000000..87b4ba7
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/RequestTest.php
@@ -0,0 +1,135 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class RequestTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->streams = array(
+ 'input' => LITHIUM_APP_PATH . '/tmp/input.txt',
+ );
+
+ $this->working = LITHIUM_APP_PATH;
+ if (!empty($_SERVER['PWD'])) {
+ $this->working = $_SERVER['PWD'];
+ }
+ $this->server = $_SERVER;
+ $_SERVER['argv'] = array();
+ }
+
+ public function tearDown() {
+ foreach ($this->streams as $path) {
+ if (file_exists($path)) {
+ unlink($path);
+ }
+ }
+ $_SERVER = $this->server;
+ }
+
+ public function testConstructWithoutConfig() {
+ $request = new Request();
+ $expected = array();
+ $this->assertEqual($expected, $request->args);
+
+ $expected = array('working' => $this->working);
+ $this->assertEqual($expected, $request->env);
+ }
+
+ public function testConstructWithServer() {
+ $_SERVER['PWD'] = '/path/to/console';
+ $request = new Request();
+ $expected = array();
+ $this->assertEqual($expected, $request->args);
+
+ $expected = array('working' => '/path/to/console');
+ $this->assertEqual($expected, $request->env);
+
+ $_SERVER['argv'] = array('one', 'two');
+ $request = new Request();
+ $expected = array('one', 'two');
+ $this->assertEqual($expected, $request->args);
+
+ $expected = array('working' => '/path/to/console');
+ $this->assertEqual($expected, $request->env);
+ }
+
+ public function testConstructWithConfigArgv() {
+ $request = new Request(array(
+ 'argv' => array('wrong')
+ ));
+ $expected = array('wrong');
+ $this->assertEqual($expected, $request->args);
+
+ $expected = array('working' => $this->working);
+ $this->assertEqual($expected, $request->env);
+
+ $request = new Request(array(
+ 'argv' => array('lithium.php', '-working', '/path/to/console', 'one', 'two')
+ ));
+
+ $expected = array('one', 'two');
+ $this->assertEqual($expected, $request->args);
+
+ $expected = array('working' => '/path/to/console');
+ $this->assertEqual($expected, $request->env);
+ }
+
+ public function testConstructWithConfigArgs() {
+ $request = new Request(array(
+ 'args' => array('ok')
+ ));
+ $expected = array('ok');
+ $this->assertEqual($expected, $request->args);
+
+ $request = new Request(array(
+ 'args' => array('ok'),
+ 'argv' => array('one', 'two', 'three', 'four')
+ ));
+ $expected = array('ok', 'two', 'three', 'four');
+ $this->assertEqual($expected, $request->args);
+
+ $expected = array('working' => $this->working);
+ $this->assertEqual($expected, $request->env);
+ }
+
+ public function testConstructWithEnv() {
+ $_SERVER['PWD'] = '/dont/use/this/';
+ $request = new Request(array(
+ 'env' => array('working' => '/some/other/path')
+ ));
+
+ $expected = array('working' => '/some/other/path');
+ $this->assertEqual($expected, $request->env);
+ }
+
+ public function testInput() {
+ $stream = fopen($this->streams['input'], 'w+');
+ $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);
+ rewind($request->input);
+
+ $expected = 'ok';
+ $result = $request->input();
+ $this->assertEqual($expected, $result);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/ResponseTest.php b/libraries/lithium/tests/cases/console/ResponseTest.php
new file mode 100644
index 0000000..2ca74dd
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/ResponseTest.php
@@ -0,0 +1,112 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class ResponseTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->streams = array(
+ 'output' => LITHIUM_APP_PATH . '/tmp/output.txt',
+ 'error' => LITHIUM_APP_PATH . '/tmp/error.txt'
+ );
+
+ $this->working = LITHIUM_APP_PATH;
+ if (!empty($_SERVER['PWD'])) {
+ $this->working = $_SERVER['PWD'];
+ }
+ }
+
+ public function tearDown() {
+ foreach ($this->streams as $path) {
+ if (file_exists($path)) {
+ unlink($path);
+ }
+ }
+ }
+
+ public function testConstructWithoutConfig() {
+ $response = new Response();
+ $expected = null;
+ $this->assertEqual($expected, $response->request);
+
+ $this->assertTrue(is_resource($response->output));
+
+ $this->assertTrue(is_resource($response->error));
+ }
+
+ public function testConstructWithConfigRequest() {
+ $response = new Response(array(
+ 'request' => new Request()
+ ));
+ $expected = new Request();
+ $this->assertEqual($expected->env, $response->request->env);
+
+ $expected = array('working' => $this->working);
+ $this->assertEqual($expected, $response->request->env);
+ }
+
+ public function testConstructWithConfigOutput() {
+ $stream = fopen($this->streams['output'], 'w');
+ $response = new Response(array(
+ 'output' => $stream
+ ));
+ $this->assertTrue(is_resource($response->output));
+ $this->assertEqual($stream, $response->output);
+
+ }
+
+ public function testConstructWithConfigErrror() {
+ $stream = fopen($this->streams['error'], 'w');
+ $response = new Response(array(
+ 'error' => $stream
+ ));
+ $this->assertTrue(is_resource($response->error));
+ $this->assertEqual($stream, $response->error);
+
+ }
+
+ public function testOutput() {
+ $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);
+ }
+
+ public function testError() {
+ $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);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/RouterTest.php b/libraries/lithium/tests/cases/console/RouterTest.php
new file mode 100644
index 0000000..478bbc9
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/RouterTest.php
@@ -0,0 +1,81 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class RouterTest extends \lithium\test\Unit {
+
+ public function testParseNoOptions() {
+ $expected = array(
+ 'command' => null, 'action' => 'run',
+ 'passed' => array(), 'named' => array()
+ );
+ $result = Router::parse();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testParseWithPassed() {
+ $expected = array(
+ 'command' => 'test',
+ 'action' => 'action',
+ 'passed' => array('param'),
+ 'named' => array()
+ );
+ $result = Router::parse(new Request(array(
+ 'args' => array(
+ 'test', 'action', 'param'
+ )
+ )));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testParseWithNamed() {
+ $expected = array(
+ 'command' => 'test',
+ 'action' => 'run',
+ 'passed' => array(),
+ 'named' => array(
+ 'case' => 'lithium.tests.cases.console.RouterTest'
+ )
+ );
+ $result = Router::parse(new Request(array(
+ 'args' => array(
+ 'test',
+ '-case', 'lithium.tests.cases.console.RouterTest'
+ )
+ )));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testParseWithDoubleNamed() {
+ $expected = array(
+ 'command' => 'test',
+ 'action' => 'run',
+ 'passed' => array(),
+ 'named' => array(
+ 'case' => 'lithium.tests.cases.console.RouterTest'
+ )
+ );
+ $result = Router::parse(new Request(array(
+ 'args' => array(
+ 'test',
+ '--case=lithium.tests.cases.console.RouterTest'
+ )
+ )));
+
+ $this->assertEqual($expected, $result);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/console/commands/GenerateTest.php b/libraries/lithium/tests/cases/console/commands/GenerateTest.php
new file mode 100644
index 0000000..d6aac36
--- /dev/null
+++ b/libraries/lithium/tests/cases/console/commands/GenerateTest.php
@@ -0,0 +1,22 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\console\commands;
+
+use \lithium\console\commands\Generate;
+
+class GenerateTest extends \lithium\test\Unit {
+
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/EnvironmentTest.php b/libraries/lithium/tests/cases/core/EnvironmentTest.php
new file mode 100644
index 0000000..dcf708d
--- /dev/null
+++ b/libraries/lithium/tests/cases/core/EnvironmentTest.php
@@ -0,0 +1,98 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class MockRequest extends \lithium\core\Object {
+
+ public function env($key) {
+ return $this->_config[$key];
+ }
+}
+
+class EnvironmentTest extends \lithium\test\Unit {
+
+ /**
+ * Tests setting and getting current environment, and that invalid environments cannot be
+ * selected.
+ *
+ * @return void
+ */
+ public function testSetAndGetCurrentEnvironment() {
+ Environment::set('development');
+ $this->assertEqual('development', Environment::get());
+ $this->assertEqual('development', Environment::is());
+ $this->assertTrue(Environment::is('development'));
+ $this->assertNull(Environment::get('foo'));
+ $this->assertTrue(is_array(Environment::get('development')));
+ }
+
+ /**
+ * Tests modifying environment configuration.
+ *
+ * @return void
+ */
+ public function testModifyEnvironmentConfiguration() {
+ $expected = array('inherit' => 'development', 'foo' => 'bar');
+ Environment::set('test', array('foo' => 'bar'));
+ $this->assertEqual($expected, Environment::get('test'));
+
+ $expected = array('inherit' => 'production', 'foo' => 'bar', 'baz' => 'qux');
+ Environment::set('test', array('inherit' => 'production', 'baz' => 'qux'));
+ $this->assertEqual($expected, Environment::get('test'));
+ }
+
+ /**
+ * Tests auto-detecting environment settings through a series of mock request classes.
+ *
+ * @return void
+ */
+ public function testEnvironmentDetection() {
+ Environment::set(new MockRequest(array('SERVER_ADDR' => '::1')));
+ $this->assertTrue(Environment::is('development'));
+
+ $request = new MockRequest(array('SERVER_ADDR' => '1.1.1.1', 'HTTP_HOST' => 'test.local'));
+ Environment::set($request);
+ $this->assertTrue(Environment::is('test'));
+
+ $request = new MockRequest(array('SERVER_ADDR' => '1.1.1.1', 'HTTP_HOST' => 'www.com'));
+ Environment::set($request);
+ $this->assertTrue(Environment::is('production'));
+ }
+
+ /**
+ * Tests using a custom detector to get the current environment.
+ *
+ * @return void
+ */
+ public function testCustomDetector() {
+ Environment::is(function($request) {
+ return $request->env('HTTP_HOST') == 'localhost' ? 'development' : 'production';
+ });
+
+ $request = new MockRequest(array('HTTP_HOST' => 'localhost'));
+ Environment::set($request);
+ $this->assertTrue(Environment::is('development'));
+
+ $request = new MockRequest(array('HTTP_HOST' => 'lappy.local'));
+ Environment::set($request);
+ $this->assertTrue(Environment::is('production'));
+
+ $request = new MockRequest(array('HTTP_HOST' => 'test.local'));
+ Environment::set($request);
+ $this->assertTrue(Environment::is('production'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/LibrariesTest.php b/libraries/lithium/tests/cases/core/LibrariesTest.php
new file mode 100644
index 0000000..494e503
--- /dev/null
+++ b/libraries/lithium/tests/cases/core/LibrariesTest.php
@@ -0,0 +1,146 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\core;
+
+use \lithium\core\Libraries;
+
+class LibrariesTest extends \lithium\test\Unit {
+
+ public function testNamespaceToFileTranslation() {
+ $result = Libraries::path('\lithium\core\Libraries');
+ $this->assertTrue(strpos($result, '/lithium/core/Libraries.php'));
+ $this->assertTrue(file_exists($result));
+ }
+
+ public function testPathFiltering() {
+ $tests = Libraries::find('lithium', array('recursive' => true, 'path' => '/tests/cases'));
+ $result = preg_grep('/^lithium\\\\tests\\\\cases\\\\/', $tests);
+ $this->assertIdentical($tests, $result);
+
+ $all = Libraries::find('lithium', array('recursive' => true));
+ $result = array_values(preg_grep('/^lithium\\\\tests\\\\cases\\\\/', $all));
+ $this->assertIdentical($tests, $result);
+ }
+
+ /**
+ * Tests accessing library configurations.
+ *
+ * @return void
+ */
+ public function testLibraryConfigAccess() {
+ $result = Libraries::get('lithium');
+ $expected = array(
+ 'loader' => 'lithium\\core\\Libraries::load',
+ 'path' => LITHIUM_LIBRARY_PATH . '/lithium',
+ 'prefix' => 'lithium\\',
+ 'suffix' => '.php',
+ 'transform' => null,
+ 'bootstrap' => null,
+ 'defer' => true
+ );
+ $this->assertEqual($expected, $result);
+ $this->assertNull(Libraries::get('foo'));
+
+ $result = Libraries::get();
+ $this->assertTrue(array_key_exists('lithium', $result));
+ $this->assertTrue(array_key_exists('app', $result));
+ $this->assertEqual($expected, $result['lithium']);
+ }
+
+ /**
+ * Tests the addition and removal of default libraries.
+ *
+ * @return void
+ */
+ public function testLibraryAddRemove() {
+ $lithium = Libraries::get('lithium');
+ $this->assertFalse(empty($lithium));
+
+ $app = Libraries::get('app');
+ $this->assertFalse(empty($app));
+
+ Libraries::remove(array('lithium', 'app'));
+
+ $result = Libraries::get('lithium');
+ $this->assertTrue(empty($result));
+
+ $result = Libraries::get('app');
+ $this->assertTrue(empty($result));
+
+ $result = Libraries::add('lithium', array('bootstrap' => null) + $lithium);
+ $this->assertEqual($lithium, $result);
+
+ $result = Libraries::add('app', array('bootstrap' => null) + $app);
+ $this->assertEqual(array('bootstrap' => null) + $app, $result);
+ }
+
+ /**
+ * Tests path caching by calling `path()` twice.
+ *
+ * @return void
+ */
+ public function testPathCaching() {
+ $path = Libraries::path(__CLASS__);
+ $result = Libraries::path(__CLASS__);
+ $this->assertEqual($path, $result);
+ }
+
+ /**
+ * Tests recursive and non-recursive searching through libraries with paths.
+ *
+ * @return void
+ */
+ public function testFindingClasses() {
+ $result = Libraries::find('lithium', array(
+ 'recursive' => true, 'path' => '/tests/cases', 'filter' => '/LibrariesTest/'
+ ));
+ $this->assertIdentical(array(__CLASS__), $result);
+
+ $result = Libraries::find('lithium', array(
+ 'path' => '/tests/cases/', 'filter' => '/LibrariesTest/'
+ ));
+ $this->assertIdentical(array(), $result);
+
+ $result = Libraries::find('lithium', array(
+ 'path' => '/tests/cases/core', 'filter' => '/LibrariesTest/'
+ ));
+ $this->assertIdentical(array(__CLASS__), $result);
+
+ $count = Libraries::find('lithium', array('recursive' => true));
+ $count2 = Libraries::find(true, array('recursive' => true));
+ $this->assertTrue($count < $count2);
+
+ $result = Libraries::find('foo', array('recursive' => true));
+ $this->assertNull($result);
+ }
+
+ /**
+ * Tests locating service objects. These tests may fail if not run on a stock install, as other
+ * objects may preceed the core objects in load order.
+ *
+ * @return void
+ */
+ public function testServiceLocation() {
+ $this->assertNull(Libraries::locate('adapters.view', 'File'));
+
+ $result = Libraries::locate('adapters.template.view', 'File');
+ $this->assertEqual('lithium\template\view\adapters\File', $result);
+
+ $result = Libraries::locate('adapters.storage.cache', 'File');
+ $expected = 'lithium\storage\cache\adapters\File';
+ $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
new file mode 100644
index 0000000..164b16f
--- /dev/null
+++ b/libraries/lithium/tests/cases/core/ObjectTest.php
@@ -0,0 +1,179 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class TestMethodFiltering extends \lithium\core\Object {
+
+ public function method($data) {
+ $data[] = 'Starting outer method call';
+ $result = $this->_filter(__METHOD__, compact('data'), function($self, $params, $chain) {
+ $params['data'][] = 'Inside method implementation';
+ return $params['data'];
+ });
+ $result[] = 'Ending outer method call';
+ return $result;
+ }
+
+ public function method2() {
+ $filters =& $this->_methodFilters;
+ $method = function($self, $params, $chain) use (&$filters) {
+ return $filters;
+ };
+ return $this->_filter(__METHOD__, array(), $method);
+ }
+}
+
+class Exposed extends \lithium\core\Object {
+
+ protected $_internal = 'secret';
+
+ public function tamper() {
+ $internal =& $this->_internal;
+
+ return $this->_filter(__METHOD__, array(), function() use (&$internal) {
+ $internal = 'tampered';
+ return true;
+ });
+ }
+
+ public function get() {
+ return $this->_internal;
+ }
+}
+
+class Callable extends \lithium\core\Object {
+
+ public function __call($method, $params = array()) {
+ return $params;
+ }
+}
+
+class ObjectTest extends \lithium\test\Unit {
+
+ public function testMethodFiltering() {
+ $test = new TestMethodFiltering();
+ $result = $test->method(array('Starting test'));
+ $expected = array(
+ 'Starting test',
+ 'Starting outer method call',
+ 'Inside method implementation',
+ 'Ending outer method call'
+ );
+ $this->assertEqual($expected, $result);
+
+ $test->applyFilter('method', function($self, $params, $chain) {
+ $params['data'][] = 'Starting filter';
+ $result = $chain->next($self, $params, $chain);
+ $result[] = 'Ending filter';
+ return $result;
+ });
+
+ $result = $test->method(array('Starting test'));
+ $expected = array(
+ 'Starting test',
+ 'Starting outer method call',
+ 'Starting filter',
+ 'Inside method implementation',
+ 'Ending filter',
+ 'Ending outer method call'
+ );
+ $this->assertEqual($expected, $result);
+
+ $test->applyFilter('method', function($self, $params, $chain) {
+ $params['data'][] = 'Starting inner filter';
+ $result = $chain->next($self, $params, $chain);
+ $result[] = 'Ending inner filter';
+ return $result;
+ });
+ $result = $test->method(array('Starting test'));
+ $expected = array(
+ 'Starting test',
+ 'Starting outer method call',
+ 'Starting filter',
+ 'Starting inner filter',
+ 'Inside method implementation',
+ 'Ending inner filter',
+ 'Ending filter',
+ 'Ending outer method call'
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Verifies workaround for accessing protected properties in filtered methods.
+ *
+ * @return void
+ */
+ function testFilteringWithProtectedAccess() {
+ $object = new Exposed();
+ $this->assertEqual($object->get(), 'secret');
+ $this->assertTrue($object->tamper());
+ $this->assertEqual($object->get(), 'tampered');
+ }
+
+ /**
+ * Attaches a single filter to multiple methods.
+ *
+ * @return void
+ */
+ function testMultipleMethodFiltering() {
+ $object = new TestMethodFiltering();
+ $this->assertIdentical($object->method2(), array());
+
+ $object->applyFilter(array('method', 'method2'), function($self, $params, $chain) {
+ return $chain->next($self, $params, $chain);
+ });
+ $this->assertIdentical(array_keys($object->method2()), array('method', 'method2'));
+ }
+
+ /**
+ * Tests that the correct parameters are always passed in Object::invokeMethod(), regardless of
+ * the number.
+ *
+ * @return void
+ */
+ public function testMethodInvokationWithParameters() {
+ $callable = new Callable();
+
+ $this->assertEqual($callable->invokeMethod('foo'), array());
+ $this->assertEqual($callable->invokeMethod('foo', array('bar')), array('bar'));
+
+ $params = array('one', 'two');
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+
+ $params = array('short', 'parameter', 'list');
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+
+ $params = array('a', 'longer', 'parameter', 'list');
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+
+ $params = array('a', 'much', 'longer', 'parameter', 'list');
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+
+ $params = array('an', 'extremely', 'long', 'list', 'of', 'parameters');
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+
+ $params = array('an', 'extremely', 'long', 'list', 'of', 'parameters');
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+
+ $params = array(
+ 'if', 'you', 'have', 'a', 'parameter', 'list', 'this',
+ 'long', 'then', 'UR', 'DOIN', 'IT', 'RONG'
+ );
+ $this->assertEqual($callable->invokeMethod('foo', $params), $params);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/core/StaticObjectTest.php b/libraries/lithium/tests/cases/core/StaticObjectTest.php
new file mode 100644
index 0000000..3bb7402
--- /dev/null
+++ b/libraries/lithium/tests/cases/core/StaticObjectTest.php
@@ -0,0 +1,133 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class TestMethodFilteringStatic extends \lithium\core\StaticObject {
+
+ public static function method($data) {
+ $data[] = 'Starting outer method call';
+ $result = static::_filter(__METHOD__, compact('data'), function($self, $params, $chain) {
+ $params['data'][] = 'Inside method implementation of ' . $self;
+ return $params['data'];
+ });
+ $result[] = 'Ending outer method call';
+ return $result;
+ }
+
+ public static function method2() {
+ $filters =& static::$_methodFilters;
+ $method = function($self, $params, $chain) use (&$filters) {
+ return $filters;
+ };
+ return static::_filter(__METHOD__, array(), $method);
+ }
+
+ public static function foo() {
+ $args = func_get_args();
+ return $args;
+ }
+}
+
+class StaticObjectTest extends \lithium\test\Unit {
+
+ public function testMethodFiltering() {
+ $class = __NAMESPACE__ . '\TestMethodFilteringStatic';
+
+ $result = $class::method(array('Starting test'));
+ $expected = array(
+ 'Starting test',
+ 'Starting outer method call',
+ 'Inside method implementation of ' . $class,
+ 'Ending outer method call'
+ );
+ $this->assertEqual($expected, $result);
+
+ $class::applyFilter('method', function($self, $params, $chain) {
+ $params['data'][] = 'Starting filter';
+ $result = $chain->next($self, $params, $chain);
+ $result[] = 'Ending filter';
+ return $result;
+ });
+
+ $result = $class::method(array('Starting test'));
+ $expected = array(
+ 'Starting test',
+ 'Starting outer method call',
+ 'Starting filter',
+ 'Inside method implementation of ' . $class,
+ 'Ending filter',
+ 'Ending outer method call'
+ );
+ $this->assertEqual($expected, $result);
+
+ $class::applyFilter('method', function($self, $params, $chain) {
+ $params['data'][] = 'Starting inner filter';
+ $result = $chain->next($self, $params, $chain);
+ $result[] = 'Ending inner filter';
+ return $result;
+ });
+ $result = $class::method(array('Starting test'));
+ $expected = array(
+ 'Starting test',
+ 'Starting outer method call',
+ 'Starting filter',
+ 'Starting inner filter',
+ 'Inside method implementation of ' . $class,
+ 'Ending inner filter',
+ 'Ending filter',
+ 'Ending outer method call'
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests that the correct parameters are always passed in StaticObject::invokeMethod(),
+ * regardless of the number.
+ *
+ * @return void
+ */
+ public function testMethodInvokationWithParameters() {
+ $class = __NAMESPACE__ . '\TestMethodFilteringStatic';
+
+ $this->assertEqual($class::invokeMethod('foo'), array());
+ $this->assertEqual($class::invokeMethod('foo', array('bar')), array('bar'));
+
+ $params = array('one', 'two');
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+
+ $params = array('short', 'parameter', 'list');
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+
+ $params = array('a', 'longer', 'parameter', 'list');
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+
+ $params = array('a', 'much', 'longer', 'parameter', 'list');
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+
+ $params = array('an', 'extremely', 'long', 'list', 'of', 'parameters');
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+
+ $params = array('an', 'extremely', 'long', 'list', 'of', 'parameters');
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+
+ $params = array(
+ 'if', 'you', 'have', 'a', 'parameter', 'list', 'this',
+ 'long', 'then', 'UR', 'DOIN', 'IT', 'RONG'
+ );
+ $this->assertEqual($class::invokeMethod('foo', $params), $params);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/ConnectionsTest.php b/libraries/lithium/tests/cases/data/ConnectionsTest.php
new file mode 100644
index 0000000..8522b50
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/ConnectionsTest.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class ConnectionsTest extends \lithium\test\Unit {
+
+ public $config = array(
+ 'adapter' => 'MySql',
+ 'host' => 'localhost',
+ 'login' => '--user--',
+ 'password' => '--pass--',
+ 'database' => 'db'
+ );
+
+ protected $_preserved = array();
+
+ public function setUp() {
+ if (empty($this->_preserved)) {
+ foreach (Connections::get() as $conn) {
+ $this->_preserved[$conn] = Connections::get($conn, array('config' => true));
+ }
+ }
+ Connections::clear();
+ }
+
+ public function tearDown() {
+ foreach ($this->_preserved as $name => $config) {
+ Connections::add($name, $config['type'], $config);
+ }
+ }
+
+ public function testConnectionCreate() {
+ $result = Connections::add('conn-test', 'Database', $this->config);
+ $expected = $this->config + array('type' => 'Database');
+ $this->assertEqual($expected, $result);
+
+ $this->expectException('/mysql_get_server_info/');
+ $this->expectException('/mysql_select_db/');
+ $this->expectException('/mysql_connect/');
+ $result = Connections::get('conn-test');
+ $this->assertTrue($result instanceof \lithium\data\source\database\adapter\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/');
+ $result = Connections::get('conn-test-2');
+ $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+ }
+
+ public function testConnectionGetAndReset() {
+ Connections::add('conn-test', $this->config);
+ Connections::add('conn-test-2', $this->config);
+ $this->assertEqual(array('conn-test', 'conn-test-2'), Connections::get());
+
+ $expected = $this->config + array('type' => 'Database');
+ $this->assertEqual($expected, Connections::get('conn-test', array('config' => true)));
+
+ $this->assertNull(Connections::clear());
+ $this->assertFalse(Connections::get());
+
+ Connections::__init();
+ $this->assertTrue(Connections::get());
+ }
+
+ public function testConnectionAutoInstantiation() {
+ Connections::add('conn-test', $this->config);
+ Connections::add('conn-test-2', $this->config);
+
+ $this->expectException('/mysql_get_server_info/');
+ $this->expectException('/mysql_select_db/');
+ $this->expectException('/mysql_connect/');
+ $result = Connections::get('conn-test');
+ $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+
+ $result = Connections::get('conn-test');
+ $this->assertTrue($result instanceof \lithium\data\source\database\adapter\MySql);
+
+ $this->assertNull(Connections::get('conn-test-2', array('autoBuild' => false)));
+ }
+
+ public function testInvalidConnection() {
+ $this->assertNull(Connections::get('conn-invalid'));
+ }
+
+ public function testStreamConnection() {
+ $config = array(
+ 'adapter' => 'Stream',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => '80'
+ );
+
+ Connections::add('stream-test', 'Http', $config);
+ $result = Connections::get('stream-test');
+ $this->assertTrue($result instanceof \lithium\data\source\http\adapter\Stream);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/ModelTest.php b/libraries/lithium/tests/cases/data/ModelTest.php
new file mode 100644
index 0000000..61b7bad
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/ModelTest.php
@@ -0,0 +1,149 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Post extends Model {
+
+ public static function resetSchema() {
+ static::_instance()->_schema = array();
+ }
+
+ public static function instances() {
+ return array_keys(static::$_instances);
+ }
+}
+
+class Comment extends Model {
+
+ protected $_meta = array('key' => 'comment_id');
+}
+
+class Tag extends Model {
+}
+
+class Tagging extends Model {
+
+ protected $_meta = array('source' => 'posts_tags', 'key' => array('post_id', 'tag_id'));
+}
+
+class ModelTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Post::__init();
+ Comment::__init();
+ }
+
+ public function testClassInitialization() {
+ $expected = Post::instances();
+ Post::__init();
+ $this->assertEqual($expected, Post::instances());
+
+ Model::__init();
+ $this->assertEqual($expected, Post::instances());
+
+ $this->assertEqual('posts', Post::meta('source'));
+
+ Post::init(array('source' => 'post'));
+ $this->assertEqual('post', Post::meta('source'));
+
+ Post::init(array('source' => false));
+ $this->assertIdentical(false, Post::meta('source'));
+
+ Post::init(array('source' => null));
+ $this->assertIdentical('posts', Post::meta('source'));
+ }
+
+ public function testMetaInformation() {
+ $expected = array(
+ 'class' => __NAMESPACE__ . '\Post',
+ 'name' => 'Post',
+ 'key' => 'id',
+ 'title' => 'title',
+ 'source' => 'posts',
+ 'prefix' => '',
+ 'connection' => 'default'
+ );
+ $this->assertEqual($expected, Post::meta());
+
+ $expected = array(
+ 'class' => __NAMESPACE__ . '\Comment',
+ 'name' => 'Comment',
+ 'key' => 'comment_id',
+ 'title' => 'comment_id',
+ 'source' => 'comments',
+ 'prefix' => '',
+ 'connection' => 'default'
+ );
+ $this->assertEqual($expected, Comment::meta());
+
+ $expected += array('foo' => 'bar');
+ $this->assertEqual($expected, Comment::meta('foo', 'bar'));
+
+ $expected += array('bar' => true, 'baz' => false);
+ $this->assertEqual($expected, Comment::meta(array('bar' => true, 'baz' => false)));
+ }
+
+ public function testSchemaLoading() {
+ $result = Post::schema();
+ $this->assertTrue($result);
+
+ Post::resetSchema();
+ $this->assertEqual($result, Post::schema());
+ }
+
+ public function testSimpleFind() {
+ $result = Post::find('all', array('limit' => 5));
+ $this->assertTrue($result instanceof \lithium\data\model\RecordSet);
+ //$this->assertEqual(5, count($result));
+ }
+
+ public function testFilteredFind() {
+ Post::applyFilter('find', function($self, $params, $chain) {
+ $result = $chain->next($self, $params, $chain);
+ return $result;
+ });
+ }
+
+ public function testCustomFinder() {
+ $finder = function() {};
+ Post::finder('custom', $finder);
+ $this->assertIdentical($finder, Post::finder('custom'));
+ }
+
+ public function testCustomFindMethods() {
+ print_r(Post::findFirstById());
+ }
+
+ public function testKeyGeneration() {
+ $this->assertEqual('comment_id', Comment::key());
+ $this->assertEqual(array('post_id', 'tag_id'), Tagging::key());
+
+ $result = Comment::key(array('comment_id' => 5, 'body' => 'This is a comment'));
+ $this->assertEqual(5, $result);
+
+ $result = Tagging::key(array(
+ 'post_id' => 2,
+ 'tag_id' => 5,
+ 'created' => '2009-06-16 10:00:00'
+ ));
+ $this->assertEqual(array('post_id' => 2, 'tag_id' => 5), $result);
+ }
+
+ public function testRelations() {
+
+ }
+}
+
+?>
\ 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
new file mode 100644
index 0000000..6cf88fe
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/model/QueryTest.php
@@ -0,0 +1,65 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\Query;
+
+class QueryPost extends \lithium\data\Model {
+
+ 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')
+ );
+}
+
+class QueryComment extends \lithium\data\Model {
+
+ protected $_schema = array(
+ 'id' => array('type' => 'integer', 'key' => 'primary'),
+ 'author_id' => array('type' => 'integer'),
+ 'comment' => array('type' => 'text'),
+ 'created' => array('type' => 'datetime'),
+ 'updated' => array('type' => 'datetime')
+ );
+}
+
+class QueryTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ QueryPost::init();
+ QueryComment::init();
+ }
+
+ /**
+ * Tests that configuration settings are delegating to matching method names
+ *
+ * @return void
+ */
+ public function testObjectConstruction() {
+ $query = new Query();
+ $this->assertFalse($query->conditions());
+
+ $query = new Query(array('conditions' => 'foo', 'fields' => array('id')));
+ $this->assertEqual($query->conditions(), array('foo'));
+ }
+
+ public function testQueryExport() {
+ $query = new Query();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/model/RecordSetTest.php b/libraries/lithium/tests/cases/data/model/RecordSetTest.php
new file mode 100644
index 0000000..3f8c145
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/model/RecordSetTest.php
@@ -0,0 +1,63 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\RecordSet;
+
+class RecordDb extends \lithium\data\source\Database {
+
+ protected $_records = array(
+ 'test1' => array(
+ )
+ );
+
+ public function connect() {
+ }
+
+ public function disconnect() {
+ }
+
+ public function encoding($encoding = null) {
+ }
+
+ public function result($type, $resource, $context) {
+ }
+
+ public function columns($query, $resource = null, $context = null) {
+ $cols = array(
+ 'test1' => array()
+ );
+ return $cols[$resource];
+ }
+
+ public function entities($_ = null) {
+ }
+
+ public function describe($entity, $meta = array()) {
+ }
+}
+
+class RecordSetTest extends \lithium\test\Unit {
+
+ protected $_recordSet = null;
+
+ public function setUp() {
+ $this->_recordSet = new RecordSet();
+ }
+
+ public function testColumnIntrospection() {
+
+ }
+}
+
+?>
diff --git a/libraries/lithium/tests/cases/data/model/RecordTest.php b/libraries/lithium/tests/cases/data/model/RecordTest.php
new file mode 100644
index 0000000..ecabfcc
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/model/RecordTest.php
@@ -0,0 +1,68 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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);
+ }
+}
+
+?>
\ 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
new file mode 100644
index 0000000..399e6b2
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/source/DatabaseTest.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\source\Database;
+
+class MyDatabasePost extends \lithium\data\Model {
+
+ public $hasMany = array('MyDatabaseComment');
+
+ protected $_schema = array(
+ 'id' => array('type' => 'integer'),
+ 'title' => array('type' => 'string'),
+ 'created' => array('type' => 'datetime')
+ );
+}
+
+class MyDatabaseComment extends \lithium\data\Model {
+
+ public $belongsTo = array('MyDatabasePost');
+
+ protected $_schema = array(
+ 'id' => array('type' => 'integer'),
+ 'post_id' => array('type' => 'integer'),
+ 'author_id' => array('type' => 'integer'),
+ 'body' => array('type' => 'text'),
+ 'created' => array('type' => 'datetime')
+ );
+}
+
+class MyDatabase extends Database {
+
+ public function connect() {
+ }
+
+ public function disconnect() {}
+
+ public function entities($class = null) {}
+
+ public function encoding($encoding = null) {}
+
+ public function result($type, $resource, $context) {}
+
+ public function describe($entity, $meta = array()) {}
+}
+
+class DatabaseTest extends \lithium\test\Unit {
+
+ public $db = null;
+
+ public function setUp() {
+ $this->db = new MyDatabase();
+ MyDatabasePost::__init();
+ MyDatabaseComment::__init();
+ }
+
+ public function testColumnMapping() {
+ $ns = __NAMESPACE__;
+
+ $result = $this->db->columns(new Query(array('model' => "{$ns}\MyDatabasePost")));
+ $expected = array("{$ns}\MyDatabasePost" => array('id', 'title', 'created'));
+ $this->assertEqual($expected, $result);
+
+ $query = new Query(array('model' => "{$ns}\MyDatabasePost", 'fields' => array('*')));
+ $result = $this->db->columns($query);
+ $this->assertEqual($expected, $result);
+
+ $fields = array('MyDatabaseComment');
+ $query = new Query(array('model' => "{$ns}\MyDatabasePost", 'fields' => $fields));
+ $result = $this->db->columns($query);
+ $expected = array("{$ns}\MyDatabaseComment" => array_keys(MyDatabaseComment::schema()));
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/HttpTest.php b/libraries/lithium/tests/cases/data/source/HttpTest.php
new file mode 100644
index 0000000..037ae2d
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/source/HttpTest.php
@@ -0,0 +1,209 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\data\source;
+
+class SocketMock extends \lithium\util\Socket {
+
+ public function open() {
+ return true;
+ }
+
+ public function close() {
+ return true;
+ }
+
+ public function eof() {
+ return true;
+ }
+
+ 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!'
+ ));
+ }
+
+ public function write($data) {
+ return $data;
+ }
+
+ public function timeout($time) {
+ return true;
+ }
+
+ public function encoding($charset) {
+ return true;
+ }
+}
+
+class HttpMock extends \lithium\data\source\Http {
+
+ public function response($message) {
+ $this->response = new $this->_classes['response'](compact('message'));
+ return $this->_response->body;
+ }
+}
+
+class HttpTest extends \lithium\test\Unit {
+
+ protected $_testConfig = array(
+ 'adapter' => '\lithium\tests\cases\data\source\SocketMock',
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 2
+ );
+
+ public function testAllMethodsNoConnection() {
+ $http = new HttpMock(array('protocol' => null));
+ $this->assertFalse($http->connect());
+ $this->assertTrue($http->disconnect());
+ $this->assertFalse($http->get());
+ $this->assertFalse($http->post());
+ $this->assertFalse($http->put());
+ $this->assertFalse($http->del());
+ }
+
+ public function testConnect() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->connect();
+ $this->assertTrue($result);
+ }
+
+ public function testDisconnect() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->connect();
+ $this->assertTrue($result);
+
+ $result = $http->disconnect();
+ $this->assertTrue($result);
+ }
+
+ public function testEntities() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->entities();
+ }
+
+ public function testDescribe() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->describe(null, null);
+ }
+
+ public function testGet() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->get();
+ $this->assertEqual('Test!', $result);
+
+ $expected = 'HTTP/1.1';
+ $result = $http->response->protocol;
+ $this->assertEqual($expected, $result);
+
+ $expected = '200';
+ $result = $http->response->status['code'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'OK';
+ $result = $http->response->status['message'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'text/html';
+ $result = $http->response->type;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'UTF-8';
+ $result = $http->response->charset;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testGetPath() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->get('search.json');
+ $this->assertEqual('Test!', $result);
+
+ $expected = 'HTTP/1.1';
+ $result = $http->response->protocol;
+ $this->assertEqual($expected, $result);
+
+ $expected = '200';
+ $result = $http->response->status['code'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'OK';
+ $result = $http->response->status['message'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'text/html';
+ $result = $http->response->type;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'UTF-8';
+ $result = $http->response->charset;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testPost() {
+ $http = new HttpMock($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)',
+ 'Authorization: Basic cm9vdDo=',
+ 'Content-Type: application/x-www-form-urlencoded',
+ 'Content-Length: 11',
+ '', 'status=cool'
+ ));
+ $result = (string)$http->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testPut() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->put();
+ $this->assertEqual('Test!', $result);
+ }
+
+ public function testDelete() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->delete(null);
+ $this->assertEqual('Test!', $result);
+ }
+
+ public function testCreate() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->create(null);
+ $this->assertEqual('Test!', $result);
+ }
+
+ public function testRead() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->read(null);
+ $this->assertEqual('Test!', $result);
+ }
+
+ public function testUpdate() {
+ $http = new HttpMock($this->_testConfig);
+ $result = $http->update(null);
+ $this->assertEqual('Test!', $result);
+ }
+}
+
+?>
\ 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
new file mode 100644
index 0000000..ede48fc
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/source/database/adapter/MySqlTest.php
@@ -0,0 +1,115 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class MySqlMock extends MySql {
+
+ public function get($var) {
+ return $this->{$var};
+ }
+}
+
+class MySqlTest extends \lithium\test\Unit {
+
+ protected $_dbConfig = array();
+
+ public $db = null;
+
+ /**
+ * Skip the test if a MySQL adapter configuration is unavailable.
+ *
+ * @return void
+ * @todo Tie into the Environment class to ensure that the test database is being used.
+ */
+ public function skip() {
+ $this->_dbConfig = Connections::get('default', array('config' => true));
+ $hasDb = (isset($this->_dbConfig['adapter']) && $this->_dbConfig['adapter'] == 'MySql');
+ $message = 'Test database is either unavailable, or not using a MySQL adapter';
+ $this->skipIf(!$hasDb, $message);
+ }
+
+ public function setUp() {
+ $this->db = new MySql($this->_dbConfig);
+ }
+
+ /**
+ * Tests that the object is initialized with the correct default values.
+ *
+ * @return void
+ */
+ public function testConstructorDefaults() {
+ $db = new MySqlMock(array('autoConnect' => false));
+ $result = $db->get('_config');
+ $expected = array(
+ 'autoConnect' => false, 'port' => '3306', 'persistent' => true, 'host' => 'localhost',
+ 'login' => 'root', 'password' => '', 'database' => 'lithium', 'init' => true
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests that this adapter can connect to the database, and that the status is properly
+ * persisted.
+ *
+ * @return void
+ */
+ 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());
+ }
+
+ public function testDatabaseEncoding() {
+ $this->assertTrue($this->db->isConnected());
+ $this->assertTrue($this->db->encoding('utf8'));
+ $this->assertEqual('UTF-8', $this->db->encoding());
+
+ $this->assertTrue($this->db->encoding('UTF-8'));
+ $this->assertEqual('UTF-8', $this->db->encoding());
+ }
+
+ public function testColumnAbstraction() {
+ $result = $this->db->invokeMethod('_column', array('varchar'));
+ $this->assertEqual(array('type' => 'string'), $result);
+
+ $result = $this->db->invokeMethod('_column', array('tinyint(1)'));
+ $this->assertEqual(array('type' => 'boolean'), $result);
+
+ $result = $this->db->invokeMethod('_column', array('varchar(255)'));
+ $this->assertEqual(array('type' => 'string', 'length' => '255'), $result);
+
+ $result = $this->db->invokeMethod('_column', array('text'));
+ $this->assertEqual(array('type' => 'text'), $result);
+
+ $result = $this->db->invokeMethod('_column', array('text'));
+ $this->assertEqual(array('type' => 'text'), $result);
+
+ $result = $this->db->invokeMethod('_column', array('decimal(12,2)'));
+ $this->assertEqual(array('type' => 'float', 'length' => '12,2'), $result);
+
+ $result = $this->db->invokeMethod('_column', array('int(11)'));
+ $this->assertEqual(array('type' => 'integer', 'length' => '11'), $result);
+ }
+
+ public function testAbstractColumnResolution() {
+
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/http/adapter/CurlTest.php b/libraries/lithium/tests/cases/data/source/http/adapter/CurlTest.php
new file mode 100644
index 0000000..4b136e8
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/source/http/adapter/CurlTest.php
@@ -0,0 +1,73 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\Curl;
+
+class CurlMock extends \lithium\data\source\http\adapter\Curl {
+
+ public function resource() {
+ return $this->_connection->resource();
+ }
+}
+
+class CurlTest extends \lithium\test\Unit {
+
+ protected $_testConfig = array(
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 2
+ );
+
+ public function testAllMethodsNoConnection() {
+ $stream = new CurlMock(array('protocol' => null));
+ $this->assertFalse($stream->connect());
+ $this->assertTrue($stream->disconnect());
+ }
+
+ public function testConnect() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->connect();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testDisconnect() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->connect();
+ $this->assertTrue($result);
+
+ $result = $stream->disconnect();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertFalse(is_resource($result));
+ }
+
+ public function testGet() {
+ $this->skipIf(true, 'Curl adapter is not implemented');
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->connect();
+ $this->assertTrue($result);
+
+ $result = $stream->get();
+ $this->assertPattern("/^HTTP/", $result);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/data/source/http/adapter/StreamTest.php b/libraries/lithium/tests/cases/data/source/http/adapter/StreamTest.php
new file mode 100644
index 0000000..32d4b7e
--- /dev/null
+++ b/libraries/lithium/tests/cases/data/source/http/adapter/StreamTest.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\Stream;
+
+class StreamMock extends \lithium\data\source\http\adapter\Stream {
+
+ public function resource() {
+ return $this->_connection->resource();
+ }
+}
+
+class StreamTest extends \lithium\test\Unit {
+
+ protected $_testConfig = array(
+ 'adapter' => 'Stream',
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 2
+ );
+
+ public function testAllMethodsNoConnection() {
+ $stream = new StreamMock(array('protocol' => null));
+ $this->assertFalse($stream->connect());
+ $this->assertTrue($stream->disconnect());
+ }
+
+ public function testConnect() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->connect();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testDisconnect() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->connect();
+ $this->assertTrue($result);
+
+ $result = $stream->disconnect();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertFalse(is_resource($result));
+ }
+
+ public function testGet() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->connect();
+ $this->assertTrue($result);
+
+ $result = $stream->get();
+ $this->assertTrue($result);
+
+ $expected = 'HTTP/1.1';
+ $result = $stream->response->protocol;
+ $this->assertEqual($expected, $result);
+
+ $expected = '200';
+ $result = $stream->response->status['code'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'OK';
+ $result = $stream->response->status['message'];
+ $this->assertEqual($expected, $result);
+
+ $expected = 'text/html';
+ $result = $stream->response->type;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'ISO-8859-1';
+ $result = $stream->response->charset;
+ $this->assertEqual($expected, $result);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/CatalogTest.php b/libraries/lithium/tests/cases/g11n/CatalogTest.php
new file mode 100644
index 0000000..56d522e
--- /dev/null
+++ b/libraries/lithium/tests/cases/g11n/CatalogTest.php
@@ -0,0 +1,461 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\adapters\Memory;
+
+class CatalogTest extends \lithium\test\Unit {
+
+ protected $_backups = array();
+
+ public function setUp() {
+ $this->_backups['catalogConfig'] = Catalog::config()->to('array');
+ Catalog::clear();
+ Catalog::config(array(
+ 'runtime' => array('adapter' => new Memory())
+ ));
+ }
+
+ public function tearDown() {
+ Catalog::clear();
+ Catalog::config($this->_backups['catalogConfig']);
+ }
+
+ /**
+ * Tests configuration.
+ *
+ * @return void
+ */
+ public function testConfig() {}
+
+ /**
+ * Tests if configurations are cleared.
+ *
+ * @return void
+ */
+ public function testClear() {
+ $this->assertTrue(Catalog::config()->count());
+ Catalog::clear();
+ $this->assertFalse(Catalog::config()->count());
+ }
+
+ public function testDescribe() {}
+
+ /**
+ * Tests for values returned by `read()`.
+ *
+ * @return void
+ */
+ public function testRead() {
+ $result = Catalog::read('validation.ssn', 'de');
+ $this->assertNull($result);
+ }
+
+ /**
+ * Tests writing and reading for single items and locales as well as
+ * for multiple items and locales. The ouput format should be consistent between
+ * all cases.
+ *
+ * @return void
+ */
+ public function testWriteRead() {
+ $data = array(
+ 'en_US' => '/postalCode en_US/'
+ );
+ Catalog::write('validation.postalCode', $data, array('name' => 'runtime'));
+ $result = Catalog::read('validation.postalCode', 'en_US');
+ $this->assertEqual($data, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'en_US' => '/postalCode en_US/',
+ 'de_DE' => '/postalCode de_DE/'
+ );
+ Catalog::write('validation.postalCode', $data, array('name' => 'runtime'));
+ $result = Catalog::read('validation.postalCode', array('en_US', 'de_DE'));
+ $this->assertEqual($data, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'GRD' => 'Griechische Drachme',
+ 'DKK' => 'Dänische Krone'
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.currency', 'de');
+ $this->assertEqual($data, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'GRD' => 'Griechische Drachme',
+ 'DKK' => 'Dänische Krone'
+ ),
+ 'fr' => array(
+ 'GRD' => 'rachme grecque',
+ 'DKK' => 'couronne danoise'
+ ),
+ 'en' => array(
+ 'GRD' => 'Greek Drachma',
+ 'DKK' => 'Danish Krone'
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.currency', array('fr', 'en'));
+ unset($data['de']);
+ $this->assertEqual($data, $result);
+ }
+
+ /**
+ * Tests writing and reading with data merged between locales. Actual merging happens only
+ * for lists i.e. `message.page`. Only complete items are merged in, (atomic) merging between
+ * items should not occur. Categories like `validation.postalCode` fall back to results
+ * for more generic locales.
+ *
+ * @return void
+ */
+ public function testWriteReadMergeLocales() {
+ $data = array(
+ 'en' => '/postalCode en/'
+ );
+ Catalog::write('validation.postalCode', $data, array('name' => 'runtime'));
+ $result = Catalog::read('validation.postalCode', 'en_US');
+ $expected = array(
+ 'en_US' => '/postalCode en/'
+ );
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'en_US' => '/postalCode en_US/',
+ 'en' => '/postalCode en/'
+ );
+ Catalog::write('validation.postalCode', $data, array('name' => 'runtime'));
+ $result = Catalog::read('validation.postalCode', 'en_US');
+ $expected = array(
+ 'en_US' => '/postalCode en_US/'
+ );
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'en' => array('a' => true, 'b' => true, 'c' => true)
+ );
+ Catalog::write('list.language', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.language', 'en_US');
+ $expected = array(
+ 'en_US' => array('a' => true, 'b' => true, 'c' => true)
+ );
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'en' => array('a' => true, 'c' => true),
+ 'en_US' => array('b' => true)
+ );
+ Catalog::write('list.language', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.language', 'en_US');
+ $expected = array(
+ 'en_US' => array('a' => true, 'b' => true, 'c' => true)
+ );
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'DKK' => 'Dänische Krone'
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.currency', 'de_CH');
+ $expected = array(
+ 'de_CH' => array(
+ 'DKK' => 'Dänische Krone'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'DKK' => 'Dänische Krone'
+ ),
+ 'de_CH' => array(
+ 'GRD' => 'Griechische Drachme',
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.currency', 'de_CH');
+ $expected = array(
+ 'de_CH' => array(
+ 'GRD' => 'Griechische Drachme',
+ 'DKK' => 'Dänische Krone'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'GRD' => 'de Griechische Drachme',
+ 'DKK' => 'de Dänische Krone'
+ ),
+ 'de_CH' => array(
+ 'GRD' => 'de_CH Griechische Drachme',
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.currency', 'de_CH');
+ $expected = array(
+ 'de_CH' => array(
+ 'GRD' => 'de_CH Griechische Drachme',
+ 'DKK' => 'de Dänische Krone'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'GRD' => 'de Griechische Drachme',
+ 'DKK' => 'de Dänische Krone'
+ ),
+ 'de_CH' => array(
+ 'DKK' => 'de_CH Dänische Krone'
+ ),
+ 'fr' => array(
+ 'GRD' => 'fr rachme grecque',
+ 'DKK' => 'fr couronne danoise'
+ ),
+ 'fr_CH' => array(
+ 'GRD' => 'fr_CH rachme grecque',
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime'));
+ $result = Catalog::read('list.currency', array('de_CH', 'fr_CH'));
+ $expected = array(
+ 'de_CH' => array(
+ 'GRD' => 'de Griechische Drachme',
+ 'DKK' => 'de_CH Dänische Krone'
+ ),
+ 'fr_CH' => array(
+ 'GRD' => 'fr_CH rachme grecque',
+ 'DKK' => 'fr couronne danoise'
+ ));
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests that a scope is honored if one is used.
+ *
+ * @return void
+ */
+ public function testWriteReadWithScope() {
+ $data = array(
+ 'en_US' => '/postalCode en_US scope0/'
+ );
+ Catalog::write('validation.postalCode', $data, array(
+ 'name' => 'runtime',
+ 'scope' => 'scope0'
+ ));
+ $data = array(
+ 'en_US' => '/postalCode en_US scope1/'
+ );
+ Catalog::write('validation.postalCode', $data, array(
+ 'name' => 'runtime',
+ 'scope' => 'scope1'
+ ));
+
+ $result = Catalog::read('validation.postalCode', 'en_US');
+ $this->assertNull($result);
+
+ $result = Catalog::read('validation.postalCode', 'en_US', array('scope' => 'scope0'));
+ $expected = array(
+ 'en_US' => '/postalCode en_US scope0/'
+ );
+ $this->assertEqual($expected, $result);
+
+ $result = Catalog::read('validation.postalCode', 'en_US', array('scope' => 'scope1'));
+ $expected = array(
+ 'en_US' => '/postalCode en_US scope1/'
+ );
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'en_US' => '/postalCode en_US/'
+ );
+ Catalog::write('validation.postalCode', $data, array(
+ 'name' => 'runtime'
+ ));
+
+ $result = Catalog::read('validation.postalCode', 'en_US');
+ $expected = array(
+ 'en_US' => '/postalCode en_US/'
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests reading from multiple configured stores with fallbacks. The first result should
+ * be returned for i.e. `validation.postalCode` but for lists or list-like categories
+ * (i.e. `message`) results are being merged.
+ *
+ * @return void
+ */
+ public function testWriteReadMergeConfigurations() {
+ Catalog::clear();
+ Catalog::config(array(
+ 'runtime0' => array('adapter' => new Memory()),
+ 'runtime1' => array('adapter' => new Memory())
+ ));
+
+ $data = array(
+ 'en' => '/postalCode en0/'
+ );
+ Catalog::write('validation.postalCode', $data, array('name' => 'runtime0'));
+ $data = array(
+ 'en_US' => '/postalCode en_US1/',
+ 'en' => '/postalCode en1/'
+ );
+ Catalog::write('validation.postalCode', $data, array('name' => 'runtime1'));
+ $result = Catalog::read('validation.postalCode', 'en_US');
+ $expected = array(
+ 'en_US' => '/postalCode en_US1/'
+ );
+ $this->assertEqual($expected, $result);
+
+ Catalog::clear();
+ Catalog::config(array(
+ 'runtime0' => array('adapter' => new Memory()),
+ 'runtime1' => array('adapter' => new Memory())
+ ));
+
+ $data = array(
+ 'de' => array(
+ 'GRD' => 'de0 Griechische Drachme',
+ 'DKK' => 'de0 Dänische Krone'
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime0'));
+ $data = array(
+ 'de' => array(
+ 'GRD' => 'de1 Griechische Drachme'
+ ),
+ 'de_CH' => array(
+ 'GRD' => 'de_CH1 Griechische Drachme'
+ ));
+ Catalog::write('list.currency', $data, array('name' => 'runtime1'));
+ $result = Catalog::read('list.currency', 'de_CH');
+ $expected = array(
+ 'de_CH' => array(
+ 'GRD' => 'de_CH1 Griechische Drachme',
+ 'DKK' => 'de0 Dänische Krone'
+ ));
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests writing, then reading different types of values.
+ *
+ * @return void
+ */
+ public function testDataTypeSupport() {
+ $data = array('en' => function($n) { return $n == 1 ? 0 : 1; });
+ Catalog::write('message.plural', $data, array('name' => 'runtime'));
+ $result = Catalog::read('message.plural', 'en');
+ $this->assertEqual($data, $result);
+ }
+
+ /**
+ * Tests if the output is normalized and doesn't depend on the input format.
+ *
+ * @return void
+ */
+ public function testDataInputOutputFormat() {
+ $data = array(
+ 'de' => array(
+ 'house' => 'Haus'
+ ));
+ Catalog::write('message.page', $data, array('name' => 'runtime'));
+ $result = Catalog::read('message.page', 'de');
+ $expected = array(
+ 'de' => array(
+ 'house' => array(
+ 'singularId' => 'house',
+ 'pluralId' => null,
+ 'translated' => array('Haus'),
+ 'fuzzy' => false,
+ 'comments' => array(),
+ 'occurrences' => array()
+ )));
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $data = array(
+ 'de' => array(
+ 'house' => array('Haus')
+ ));
+ Catalog::write('message.page', $data, array('name' => 'runtime'));
+ $result = Catalog::read('message.page', 'de');
+ $expected = array(
+ 'de' => array(
+ 'house' => array(
+ 'singularId' => 'house',
+ 'pluralId' => null,
+ 'translated' => array('Haus'),
+ 'fuzzy' => false,
+ 'comments' => array(),
+ 'occurrences' => array()
+ )));
+ $this->assertEqual($expected, $result);
+
+ $this->tearDown();
+ $this->setUp();
+
+ $expected = array(
+ 'de' => array(
+ 'house' => array(
+ 'singularId' => 'house',
+ 'translated' => array('Haus'),
+ )));
+ Catalog::write('message.page', $data, array('name' => 'runtime'));
+ $result = Catalog::read('message.page', 'de');
+ $expected = array(
+ 'de' => array(
+ 'house' => array(
+ 'singularId' => 'house',
+ 'pluralId' => null,
+ 'translated' => array('Haus'),
+ 'fuzzy' => false,
+ 'comments' => array(),
+ 'occurrences' => array()
+ )));
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/LocaleTest.php b/libraries/lithium/tests/cases/g11n/LocaleTest.php
new file mode 100644
index 0000000..4ab2aeb
--- /dev/null
+++ b/libraries/lithium/tests/cases/g11n/LocaleTest.php
@@ -0,0 +1,285 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class LocaleTest extends \lithium\test\Unit {
+
+ /**
+ * Tests composing of a locale from tags.
+ *
+ * @return void
+ */
+ public function testCompose() {
+ $data = array(
+ 'language' => 'en',
+ );
+ $expected = 'en';
+
+ $result = Locale::compose($data);
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'language' => 'en',
+ 'territory' => 'US'
+ );
+ $expected = 'en_US';
+
+ $result = Locale::compose($data);
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'language' => 'EN',
+ 'territory' => 'US'
+ );
+ $expected = 'EN_US';
+
+ $result = Locale::compose($data);
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'language' => 'zh',
+ 'script' => 'Hans',
+ 'territory' => 'HK',
+ 'variant' => 'REVISED'
+ );
+ $expected = 'zh_Hans_HK_REVISED';
+
+ $result = Locale::compose($data);
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'territory' => 'HK',
+ 'language' => 'zh',
+ 'script' => 'Hans'
+ );
+ $expected = 'zh_Hans_HK';
+
+ $result = Locale::compose($data);
+ $this->assertEqual($expected, $result);
+
+ $result = Locale::compose(array());
+ $this->assertNull($result);
+ }
+
+ /**
+ * Tests parsing of locales formatted strictly according to
+ * the definition of the unicode locale identifier.
+ *
+ * @return void
+ */
+ public function testDecomposeStrict() {
+ $expected = array(
+ 'language' => 'en'
+ );
+ $this->assertEqual($expected, Locale::decompose('en'));
+
+ $expected = array(
+ 'language' => 'en',
+ 'territory' => 'US'
+ );
+ $this->assertEqual($expected, Locale::decompose('en_US'));
+
+ $expected = array(
+ 'language' => 'en',
+ 'territory' => 'US',
+ 'variant' => 'POSIX'
+ );
+ $this->assertEqual($expected, Locale::decompose('en_US_POSIX'));
+
+ $expected = array(
+ 'language' => 'kpe',
+ 'territory' => 'GN'
+ );
+ $this->assertEqual($expected, Locale::decompose('kpe_GN'));
+
+ $expected = array(
+ 'language' => 'zh',
+ 'script' => 'Hans'
+ );
+ $this->assertEqual($expected, Locale::decompose('zh_Hans'));
+
+ $expected = array(
+ 'language' => 'zh',
+ 'script' => 'Hans',
+ 'territory' => 'HK'
+ );
+ $this->assertEqual($expected, Locale::decompose('zh_Hans_HK'));
+
+ $expected = array(
+ 'language' => 'zh',
+ 'script' => 'Hans',
+ 'territory' => 'HK',
+ 'variant' => 'REVISED'
+ );
+ $this->assertEqual($expected, Locale::decompose('zh_Hans_HK_REVISED'));
+ }
+
+ /**
+ * Tests parsing of locales formatted loosely according to
+ * the definition of the unicode locale identifier.
+ *
+ * @return void
+ */
+ public function testDecomposeLoose() {
+ $expected = array(
+ 'language' => 'en',
+ 'territory' => 'US'
+ );
+ $this->assertEqual($expected, Locale::decompose('en-US'));
+
+ $expected = array(
+ 'language' => 'en',
+ 'territory' => 'US',
+ 'variant' => 'posiX'
+ );
+ $this->assertEqual($expected, Locale::decompose('en_US-posiX'));
+
+ $expected = array(
+ 'language' => 'kpe',
+ 'territory' => 'gn'
+ );
+ $this->assertEqual($expected, Locale::decompose('kpe_gn'));
+
+ $expected = array(
+ 'language' => 'ZH',
+ 'script' => 'HANS',
+ 'territory' => 'HK',
+ 'variant' => 'REVISED'
+ );
+ $this->assertEqual($expected, Locale::decompose('ZH-HANS-HK_REVISED'));
+ }
+
+ /**
+ * Tests failing of parsing invalid locales.
+ *
+ * @return void
+ */
+ public function testDecomposeFail() {
+ $this->expectException();
+ try {
+ Locale::decompose('deee_DE');
+ $this->assert(false);
+ } catch (Exception $e) {
+ $this->assert(true);
+ }
+
+ $this->expectException();
+ try {
+ Locale::decompose('ZH-HANS-HK_REVISED_INVALID');
+ $this->assert(false);
+ } catch (Exception $e) {
+ $this->assert(true);
+ }
+ }
+
+ /**
+ * Tests parsing of locales using shortcut methods.
+ *
+ * @return void
+ */
+ public function testDecomposeUsingShortcutMethods() {
+ $this->assertEqual('zh', Locale::language('zh_Hans_HK_REVISED'));
+ $this->assertEqual('Hans', Locale::script('zh_Hans_HK_REVISED'));
+ $this->assertEqual('HK', Locale::territory('zh_Hans_HK_REVISED'));
+ $this->assertEqual('REVISED', Locale::variant('zh_Hans_HK_REVISED'));
+
+ $this->assertNull(Locale::script('zh_HK'));
+ $this->assertNull(Locale::territory('zh'));
+ $this->assertNull(Locale::variant('zh'));
+
+ $this->expectException();
+ try {
+ Locale::notAValidTag('zh_Hans_HK_REVISED');
+ $this->assert(false);
+ } catch (Exception $e) {
+ $this->assert(true);
+ }
+ }
+
+ /**
+ * Tests if the ouput of `compose()` can be used as the input for `decompose()`
+ * and vice versa.
+ *
+ * @return void
+ */
+ public function testComposeDecomposeCompose() {
+ $data = array(
+ 'language' => 'en',
+ );
+ $expected = 'en';
+
+ $result = Locale::compose(Locale::decompose(Locale::compose($data)));
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'language' => 'en',
+ 'territory' => 'US'
+ );
+ $expected = 'en_US';
+
+ $result = Locale::compose(Locale::decompose(Locale::compose($data)));
+ $this->assertEqual($expected, $result);
+
+ $data = array(
+ 'language' => 'zh',
+ 'script' => 'Hans',
+ 'territory' => 'HK',
+ 'variant' => 'REVISED'
+ );
+ $expected = 'zh_Hans_HK_REVISED';
+
+ $result = Locale::compose(Locale::decompose(Locale::compose($data)));
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests cascading of locales.
+ *
+ * @return void
+ */
+ public function testCascade() {
+ $expected = array('root');
+ $this->assertEqual($expected, Locale::cascade('root'));
+
+ $expected = array('en', 'root');
+ $this->assertEqual($expected, Locale::cascade('en'));
+
+ $expected = array('en_US', 'en', 'root');
+ $this->assertEqual($expected, Locale::cascade('en_US'));
+
+ $expected = array('zh_HK_REVISED', 'zh_HK', 'zh', 'root');
+ $this->assertEqual($expected, Locale::cascade('zh_HK_REVISED'));
+
+ $expected = array('zh_Hans_HK', 'zh_Hans', 'zh', 'root');
+ $this->assertEqual($expected, Locale::cascade('zh_Hans_HK'));
+
+ $expected = array('zh_Hans_HK_REVISED', 'zh_Hans_HK', 'zh_Hans', 'zh', 'root');
+ $this->assertEqual($expected, Locale::cascade('zh_Hans_HK_REVISED'));
+ }
+
+ /**
+ * Tests formatting of locale.
+ *
+ * @return void
+ */
+ public function testCanonicalize() {
+ $this->assertEqual('en_US', Locale::canonicalize('en-US'));
+ $this->assertEqual('en_US_POSIX', Locale::canonicalize('en_US-posiX'));
+ $this->assertEqual('kpe_GN', Locale::canonicalize('kpe_gn'));
+ $this->assertEqual('zh_Hans_HK_REVISED', Locale::canonicalize('ZH-HANS-HK_REVISED'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/MessageTest.php b/libraries/lithium/tests/cases/g11n/MessageTest.php
new file mode 100644
index 0000000..619fce9
--- /dev/null
+++ b/libraries/lithium/tests/cases/g11n/MessageTest.php
@@ -0,0 +1,72 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\adapters\Memory;
+
+class MessageTest extends \lithium\test\Unit {
+
+ protected $_backups = array();
+
+ protected $_locale;
+
+ protected $_connection;
+
+ public function setUp() {
+ // $this->_backups['locale'] = Environment::get('G11n.locale');
+ $this->_backups['catalogConfig'] = Catalog::config()->to('array');
+ Catalog::clear();
+ Catalog::config(array(
+ 'runtime' => array('adapter' => new Memory())
+ ));
+ }
+
+ public function tearDown() {
+ Catalog::clear();
+ Catalog::config($this->_backups['catalogConfig']);
+ // Environment::set('G11n.locale', $this->_backup['locale']);
+ }
+
+ public function testTranslate() {
+ // Environment::set('G11n.locale', 'de');
+
+ $data = array(
+ 'de' => function($n) { return $n == 1 ? 0 : 1; }
+ );
+ Catalog::write('message.plural', $data, array('name' => 'runtime'));
+
+ $data = array(
+ 'de' => array(
+ 'lithium' => 'Kuchen',
+ 'house' => array('Haus', 'Häuser')
+ ));
+ Catalog::write('message.page', $data, array('name' => 'runtime'));
+
+ $expected = 'Kuchen';
+ $result = Message::translate('lithium');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Haus';
+ $result = Message::translate('house');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Häuser';
+ $result = Message::translate('house', array('count' => 5));
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapters/CldrTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapters/CldrTest.php
new file mode 100644
index 0000000..7193e99
--- /dev/null
+++ b/libraries/lithium/tests/cases/g11n/catalog/adapters/CldrTest.php
@@ -0,0 +1,67 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\g11n\catalog\adapters;
+
+use \Exception;
+use \lithium\g11n\catalog\adapters\Cldr;
+
+class CldrTest extends \lithium\test\Unit {
+
+ public $adapter;
+
+ /**
+ * Skip the test if data needed by the adapter cannot be found.
+ *
+ * @return void
+ */
+ public function skip() {
+ $available = is_dir(LITHIUM_APP_PATH . '/resources/cldr');
+ $this->skipIf(!$available, 'The data for needed by the cldr adapter cannot be found.');
+ }
+
+ public function setUp() {
+ $path = LITHIUM_APP_PATH . '/resources/cldr';
+ $this->adapter = new Cldr(compact('path'));
+ }
+
+ public function testRead() {
+ $result = $this->adapter->read('list.language', 'de', null);
+ $this->assertEqual($result['be'], 'Weißrussisch');
+ $this->assertEqual($result['en'], 'Englisch');
+ $this->assertEqual($result['fr'], 'Französisch');
+
+ $result = $this->adapter->read('list.language', 'de_CH', null);
+ $this->assertEqual($result['be'], 'Weissrussisch');
+
+ $result = $this->adapter->read('list.script', 'de', null);
+ $this->assertEqual($result['Cher'], 'Cherokee');
+ $this->assertEqual($result['Hans'], 'Vereinfachte Chinesische Schrift');
+
+ $result = $this->adapter->read('list.territory', 'de', null);
+ $this->assertEqual($result['US'], 'Vereinigte Staaten');
+ $this->assertEqual($result['FR'], 'Frankreich');
+
+ $result = $this->adapter->read('list.currency', 'de', null);
+ $this->assertEqual($result['DKK'], 'Dänische Krone');
+ $this->assertEqual($result['USD'], 'US-Dollar');
+ $this->assertEqual($result['EUR'], 'Euro');
+
+ $result = $this->adapter->read('validation.postalCode', 'en_CA', null);
+ $this->assertEqual('/^[ABCEGHJKLMNPRSTVXY]\d[A-Z][ ]?\d[A-Z]\d$/', $result);
+
+ $result = $this->adapter->read('validation.postalCode', 'en', null);
+ $this->assertNull($result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapters/CodeTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapters/CodeTest.php
new file mode 100644
index 0000000..63b203d
--- /dev/null
+++ b/libraries/lithium/tests/cases/g11n/catalog/adapters/CodeTest.php
@@ -0,0 +1,95 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\g11n\catalog\adapters;
+
+use \lithium\g11n\catalog\adapters\Code;
+
+if (false) {
+ $t('message 1');
+ $t('message 2', array('a' => 'b'));
+
+ $t($test['invalid']);
+ $t(32203);
+ $t('message 3', $test['invalid']);
+ $t('message 4', 32203);
+
+ $t('message\n5');
+ $t("message\n6");
+ $t("message\r\n7");
+ $t('message
+ 8');
+
+ $t('singular 1', 'plural 1');
+ $t('singular 2', 'plural 2', array('a' => 'b'));
+
+ $t('mixed 1');
+ $t('mixed 1', 'plural 3');
+}
+
+class CodeTest extends \lithium\test\Unit {
+
+ public $adapter;
+
+ public function setUp() {
+ $path = __DIR__;
+ $this->adapter = new Code(compact('path'));
+ }
+
+ /**
+ * Tests message string parsing, invalid values must be skipped.
+ *
+ * @return void
+ */
+ public function testReadMessageTemplate() {
+ $result = $this->adapter->read('message.template', 'root', null);
+
+ /* Simple */
+
+ $this->assertEqual('message 1', $result['message 1']['singularId']);
+ $this->assertFalse($result['message 1']['pluralId']);
+
+ $this->assertEqual('message 2', $result['message 2']['singularId']);
+ $this->assertFalse($result['message 2']['pluralId']);
+
+ $this->assertFalse(isset($result['32203']));
+ $this->assertFalse(isset($result[32203]));
+
+ $this->assertEqual('message 3', $result['message 3']['singularId']);
+ $this->assertFalse($result['message 3']['pluralId']);
+
+ $this->assertEqual('message 4', $result['message 4']['singularId']);
+ $this->assertFalse($result['message 4']['pluralId']);
+
+ /* Escaping */
+
+ $this->assertEqual('message\\\n5', $result['message\\\n5']['singularId']);
+ $this->assertEqual('message\n6', $result['message\n6']['singularId']);
+ $this->assertEqual('message\n7', $result['message\n7']['singularId']);
+ $this->assertEqual('message\n\t8', $result['message\n\t8']['singularId']);
+
+ /* Plurals */
+
+ $this->assertEqual('singular 1', $result['singular 1']['singularId']);
+ $this->assertEqual('plural 1', $result['singular 1']['pluralId']);
+
+ $this->assertEqual('singular 2', $result['singular 2']['singularId']);
+ $this->assertEqual('plural 2', $result['singular 2']['pluralId']);
+
+ /* Merging simple and plural message strings */
+
+ $this->assertEqual('mixed 1', $result['mixed 1']['singularId'], 'mixed 1');
+ $this->assertEqual('plural 3', $result['mixed 1']['pluralId'], 'plural 3');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/g11n/catalog/adapters/GettextTest.php b/libraries/lithium/tests/cases/g11n/catalog/adapters/GettextTest.php
new file mode 100644
index 0000000..329593c
--- /dev/null
+++ b/libraries/lithium/tests/cases/g11n/catalog/adapters/GettextTest.php
@@ -0,0 +1,71 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\g11n\catalog\adapters;
+
+use \RecursiveDirectoryIterator;
+use \RecursiveIteratorIterator;
+use \lithium\g11n\catalog\adapters\Gettext;
+
+class GettextTest extends \lithium\test\Unit {
+
+ public $adapter;
+
+ public function setUp() {
+ $this->_path = $path = LITHIUM_APP_PATH . '/tmp/tests';
+ mkdir($this->_path . '/en/LC_MESSAGES', 0755, true);
+ mkdir($this->_path . '/de/LC_MESSAGES', 0755, true);
+
+ $this->adapter = new Gettext(compact('path'));
+ }
+
+ public function tearDown() {
+ $base = new RecursiveDirectoryIterator($this->_path);
+ $iterator = new RecursiveIteratorIterator($base, RecursiveIteratorIterator::CHILD_FIRST);
+
+ foreach ($iterator as $item) {
+ $path = $item->getPathname();
+
+ if ($item->isDir()) {
+ rmdir($path);
+ } else {
+ unlink($path);
+ }
+ }
+ }
+
+ function testWriteReadMessageTemplate() {
+ $data = array(
+ 'singular 1' => array(
+ 'singularId' => 'singular 1',
+ 'pluralId' => 'plural 1',
+ 'translated' => array(),
+ 'occurrences' => array(
+ array('file' => 'test.php', 'line' => 1)
+ ),
+ 'comments' => array(
+ 'comment 1'
+ ),
+ 'fuzzy' => true,
+ )
+ );
+ $meta = array();
+
+ $this->adapter->write('message.template', 'root', null, $data);
+ $this->assertTrue(file_exists($this->_path . '/message_default.pot'));
+
+ $result = $this->adapter->read('message.template', 'root', null);
+ $this->assertEqual($data, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/http/BaseTest.php b/libraries/lithium/tests/cases/http/BaseTest.php
new file mode 100644
index 0000000..37596b7
--- /dev/null
+++ b/libraries/lithium/tests/cases/http/BaseTest.php
@@ -0,0 +1,84 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\http;
+
+use \lithium\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/http/RequestTest.php b/libraries/lithium/tests/cases/http/RequestTest.php
new file mode 100644
index 0000000..396462a
--- /dev/null
+++ b/libraries/lithium/tests/cases/http/RequestTest.php
@@ -0,0 +1,145 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\http;
+
+use \lithium\http\Request;
+
+class RequestTest extends \lithium\test\Unit {
+
+ public $request = null;
+
+ public function setUp() {
+ $this->request = new Request(array('init' => false));
+ }
+
+ public function testConstruct() {
+ $request = new Request(array(
+ 'host' => 'localhost',
+ 'port' => 443,
+ 'headers' => array('Header' => 'Value'),
+ 'body' => array('Part 1'),
+ 'params' => array('param' => 'value')
+ ));
+
+ $expected = 'localhost';
+ $result = $request->host;
+ $this->assertEqual($expected, $result);
+
+ $expected = 443;
+ $result = $request->port;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'GET';
+ $result = $request->method;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'HTTP/1.1';
+ $result = $request->protocol;
+ $this->assertEqual($expected, $result);
+
+ $expected = '1.1';
+ $result = $request->version;
+ $this->assertEqual($expected, $result);
+
+ $expected = '/';
+ $result = $request->path;
+ $this->assertEqual($expected, $result);
+
+ $expected = array('param' => 'value');
+ $result = $request->params;
+ $this->assertEqual($expected, $result);
+
+ $expected = array(
+ 'Host: localhost:443',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'Header: Value'
+ );
+ $result = $request->headers();
+ $this->assertEqual($expected, $result);
+
+ $expected = array();
+ $result = $request->cookies;
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Part 1';
+ $result = $request->body();
+ $this->assertEqual($expected, $result);
+
+ }
+
+ public function testQueryStringDefault() {
+ $expected = "?param=value¶m1=value1";
+ $result = $this->request->queryString(array('param' => 'value', 'param1' => 'value1'));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testQueryStringFormat() {
+ $expected = "?param:value;param1:value1";
+ $result = $this->request->queryString("{:key}:{:value};", array(
+ 'param' => 'value', 'param1' => 'value1'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $expected = "?param:value;param1:value1";
+ $result = $this->request->queryString(array(
+ 'param' => 'value', 'param1' => 'value1'
+ ), "{:key}:{:value};");
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToString() {
+ $expected = join("\r\n", array(
+ 'GET / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0 (Lithium)',
+ '', ''
+ ));
+ $result = (string)$this->request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToStringWithAuth() {
+ $request = new Request(array('auth' => array(
+ 'method' => 'Basic',
+ 'username' => 'root', 'password' => 'something'
+ )));
+ $expected = join("\r\n", array(
+ 'GET / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'Authorization: Basic ' . base64_encode('root:something'),
+ '', ''
+ ));
+ $result = (string)$request;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testToStringWithBody() {
+ $expected = join("\r\n", array(
+ 'GET / HTTP/1.1',
+ 'Host: localhost:80',
+ 'Connection: Close',
+ 'User-Agent: Mozilla/5.0 (Lithium)',
+ 'Content-Length: 11',
+ '', 'status=cool'
+ ));
+ $this->request->body(array('status=cool'));
+ $result = (string)$this->request;
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/http/ResponseTest.php b/libraries/lithium/tests/cases/http/ResponseTest.php
new file mode 100644
index 0000000..ca16399
--- /dev/null
+++ b/libraries/lithium/tests/cases/http/ResponseTest.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\http;
+
+use \lithium\http\Response;
+
+class ResponseTest extends \lithium\test\Unit {
+
+ public function testStatus() {
+ $response = new Response();
+
+ $expected = 'HTTP/1.1 500 Internal Server Error';
+ $result = $response->status(500);
+ $this->assertEqual($expected, $result);
+
+ $expected = 'HTTP/1.1 500 Internal Server Error';
+ $result = $response->status('500');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'HTTP/1.1 500 Internal Server Error';
+ $result = $response->status('Internal Server Error');
+ $this->assertEqual($expected, $result);
+
+ $expected = 500;
+ $result = $response->status('code', 'Internal Server Error');
+ $this->assertEqual($expected, $result);
+
+ $expected = 'Internal Server Error';
+ $result = $response->status('message', 500);
+ $this->assertEqual($expected, $result);
+
+ $expected = 'HTTP/1.1 500 Internal Server Error';
+ $result = $response->status();
+ $this->assertEqual($expected, $result);
+
+ $expected = 'HTTP/1.1 303 See Other';
+ $result = $response->status('See Other');
+ $this->assertEqual($expected, $result);
+
+ $expected = false;
+ $result = $response->status('foobar');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testParseMessage() {
+ $message = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Header: Value',
+ 'Connection: close',
+ 'Content-Type: text/html;charset=UTF-8',
+ '',
+ 'Test!'
+ ));
+
+ $response = new Response(compact('message'));
+ $this->assertEqual($message, (string)$response);
+ }
+
+ function testToString() {
+ $expected = join("\r\n", array(
+ 'HTTP/1.1 200 OK',
+ 'Header: Value',
+ 'Connection: close',
+ 'Content-Type: text/html;charset=UTF-8',
+ '',
+ 'Test!'
+ ));
+ $config = array(
+ 'protocol' => 'HTTP/1.1',
+ 'version' => '1.1',
+ 'status' => array('code' => '200', 'message' => 'OK'),
+ 'headers' => array(
+ 'Header' => 'Value',
+ 'Connection' => 'close',
+ 'Content-Type' => 'text/html;charset=UTF-8'
+ ),
+ 'type' => 'text/html',
+ 'charset' => 'UTF-8',
+ 'body' => 'Test!'
+ );
+ $response = new Response($config);
+ $this->assertEqual($expected, (string)$response);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/http/RouteTest.php b/libraries/lithium/tests/cases/http/RouteTest.php
new file mode 100644
index 0000000..0276763
--- /dev/null
+++ b/libraries/lithium/tests/cases/http/RouteTest.php
@@ -0,0 +1,321 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\http;
+
+use \lithium\http\Route;
+use \lithium\http\Request;
+
+class RouteTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ }
+
+ /**
+ * Tests the creation of routes for the base URL (i.e. '/'), and that they are matched
+ * properly given the correct parameters.
+ *
+ * @return void
+ */
+ public function testBaseRouteMatching() {
+ $route = new Route(array(
+ 'template' => '/',
+ 'params' => array('controller' => 'posts', 'action' => 'archive', 'page' => 1)
+ ));
+
+ $result = $route->match(array('controller' => 'posts', 'action' => 'archive', 'page' => 1));
+ $this->assertEqual('/', $result);
+
+ $result = $route->match(array('controller' => 'posts', 'action' => 'archive', 'page' => 2));
+ $this->assertFalse($result);
+
+ $result = $route->match(array());
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Tests that a request for the base URL (i.e. '/') returns the proper parameters, as defined
+ * by the base route.
+ *
+ * @return void
+ */
+ public function testBaseRouteParsing() {
+ $params = array('controller' => 'posts', 'action' => 'archive', 'page' => 1);
+ $route = new Route(array('template' => '/', 'params' => $params));
+ $request = new Request();
+ $request->url = '/';
+
+ $result = $route->parse($request);
+ $this->assertEqual($params, $result);
+
+ $request->url = '';
+ $result = $route->parse($request);
+ $this->assertEqual($params, $result);
+
+ $request->url = '/posts';
+ $this->assertFalse($route->parse($request));
+ }
+
+ /**
+ * Tests that simple routes with only a `{:controller}` parameter are properly matched, and
+ * anything including extra parameters or an action other than the default action are ignored.
+ *
+ * @return void
+ */
+ public function testSimpleRouteMatching() {
+ $route = new Route(array('template' => '/{:controller}'));
+
+ $result = $route->match(array('controller' => 'posts', 'action' => 'index'));
+ $this->assertEqual('/posts', $result);
+
+ $result = $route->match(array('controller' => 'users'));
+ $this->assertEqual('/users', $result);
+
+ $this->assertFalse($route->match(array('controller' => 'posts', 'action' => 'view')));
+ $this->assertFalse($route->match(array('controller' => 'posts', 'id' => 5)));
+ $this->assertFalse($route->match(array('action' => 'index')));
+ }
+
+ /**
+ * Tests that requests for base-level resource URLs (i.e. `'/posts'`) are properly parsed into
+ * the correct controller and action parameters.
+ *
+ * @return void
+ */
+ public function testSimpleRouteParsing() {
+ $route = new Route(array('template' => '/{:controller}'));
+ $request = new Request();
+
+ $request->url = '/posts';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => 'posts', 'action' => 'index'), $result);
+
+ $request->url = '/users';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => 'users', 'action' => 'index'), $result);
+
+ $request->url = '/users/index';
+ $this->assertFalse($route->parse($request));
+ }
+
+ public function testRouteMatchingWithOptionalParam() {
+ $route = new Route(array('template' => '/{:controller}/{:action}'));
+
+ $result = $route->match(array('controller' => 'posts'));
+ $this->assertEqual('/posts', $result);
+
+ $result = $route->match(array('controller' => 'users', 'action' => 'index'));
+ $this->assertEqual('/users', $result);
+
+ $result = $route->match(array('controller' => '1'));
+ $this->assertEqual('/1', $result);
+
+ $result = $route->match(array('controller' => '1', 'action' => 'view'));
+ $this->assertEqual('/1/view', $result);
+
+ $result = $route->match(array('controller' => 'users', 'action' => 'view'));
+ $this->assertEqual('/users/view', $result);
+
+ $result = $route->match(array('controller' => 'users', 'action' => 'view', 'id' => '5'));
+ $this->assertFalse($result);
+
+ $result = $route->match(array());
+ $this->assertFalse($result);
+ }
+
+ public function testRouteParsingWithOptionalParam() {
+ $route = new Route(array('template' => '/{:controller}/{:action}'));
+ $request = new Request();
+
+ $request->url = '/posts';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => 'posts', 'action' => 'index'), $result);
+
+ $request->url = '/users';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => 'users', 'action' => 'index'), $result);
+
+ $request->url = '/1';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => '1', 'action' => 'index'), $result);
+
+ $request->url = '/users/index';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => 'users', 'action' => 'index'), $result);
+
+ $request->url = '/users/view';
+ $result = $route->parse($request);
+ $this->assertEqual(array('controller' => 'users', 'action' => 'view'), $result);
+
+ $request->url = '/users/view/5';
+ $this->assertFalse($route->parse($request));
+
+ $request->url = '/';
+ $this->assertFalse($route->parse($request));
+ }
+
+ public function testRouteParsingWithOptionalParams() {
+ $route = new Route(array(
+ 'template' => '/{:controller}/{:action}/{:id}', 'params' => array('id' => null)
+ ));
+ $request = new Request();
+
+ $request->url = '/posts';
+ $result = $route->parse($request);
+ $expected = array('controller' => 'posts', 'action' => 'index', 'id' => null);
+ $this->assertEqual($expected, $result);
+
+ $request->url = '/posts/index';
+ $result = $route->parse($request);
+ $this->assertEqual($expected, $result);
+
+ $request->url = '/posts/index/';
+ $result = $route->parse($request);
+ $this->assertEqual($expected, $result);
+
+ $request->url = '/posts/view/5';
+ $result = $route->parse($request);
+ $expected = array('controller' => 'posts', 'action' => 'view', 'id' => '5');
+ $this->assertEqual($expected, $result);
+
+ $request->url = '/';
+ $this->assertFalse($route->parse($request));
+
+ $request->url = '/posts/view/5/foo';
+ $this->assertFalse($route->parse($request));
+ }
+
+ public function testRouteParsingWithOptionalParamsAndType() {
+ $route = new Route(array(
+ 'template' => '/{:controller}/{:action}/{:id}.{:type}',
+ 'params' => array('id' => null)
+ ));
+ $request = new Request();
+
+ $request->url = '/posts/view/5.xml';
+ $result = $route->parse($request);
+ $expected = array(
+ 'controller' => 'posts', 'action' => 'view', 'id' => '5', 'type' => 'xml'
+ );
+ $this->assertEqual($expected, $result);
+
+ $request->url = '/posts/index.xml';
+ $result = $route->parse($request);
+ $expected = array(
+ 'controller' => 'posts', 'action' => 'index', 'id' => '', 'type' => 'xml'
+ );
+ $this->assertEqual($expected, $result);
+
+ $request->url = '/posts.xml';
+ $result = $route->parse($request);
+ $expected = array(
+ 'controller' => 'posts', 'action' => 'index', 'id' => '', 'type' => 'xml'
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testRouteMatchingWithEmptyTrailingParams() {
+ $route = new Route(array('template' => '/{:controller}/{:action}/{:args}'));
+
+ $result = $route->match(array('controller' => 'posts'));
+ $this->assertEqual('/posts', $result);
+
+ $result = $route->match(array('controller' => 'posts', 'args' => 'foo'));
+ $this->assertEqual('/posts/index/foo', $result);
+
+ $result = $route->match(array('controller' => 'posts', 'args' => array('foo', 'bar')));
+ $this->assertEqual('/posts/index/foo/bar', $result);
+
+ $request = new Request();
+ $request->url = '/posts/index/foo/bar';
+
+ $result = $route->parse($request);
+ $expected = array(
+ 'controller' => 'posts', 'action' => 'index', 'args' => array('foo', 'bar')
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testStaticRouteMatching() {
+ $route = new Route(array('template' => '/login', 'params' => array(
+ 'controller' => 'sessions', 'action' => 'add'
+ )));
+ $result = $route->match(array('controller' => 'sessions', 'action' => 'add'));
+ $this->assertEqual('/login', $result);
+
+ $result = $route->match(array());
+ $this->assertFalse($result);
+
+ $request = new Request();
+ $expected = array('controller' => 'sessions', 'action' => 'add');
+
+ $request->url = '/login';
+ $result = $route->parse($request);
+ $this->assertEqual($expected, $result);
+
+ $request->url = 'login';
+ $result = $route->parse($request);
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests that routes can be composed of manual regular expressions.
+ *
+ * @return void
+ */
+ public function testManualRouteDefinition() {
+ $route = new Route(array(
+ 'template' => '/{:controller}',
+ 'pattern' => '/(?P<controller>[A-Za-z0-9_-]+)/',
+ 'keys' => array('controller' => 'controller'),
+ 'match' => array('action' => 'index'),
+ 'options' => array('wrap' => false, 'compile' => false)
+ ));
+
+ $request = new Request();
+ $request->url = '/posts';
+
+ $result = $route->parse($request);
+ $expected = array('controller' => 'posts', 'action' => 'index');
+ $this->assertEqual($expected, $result);
+
+ $result = $route->match(array('controller' => 'posts', 'action' => 'index'));
+ $expected = '/posts';
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests exporting a the details of a compiled route to an array.
+ *
+ * @return void
+ */
+ public function testRouteExporting() {
+ $result = new Route(array(
+ 'template' => '/{:controller}/{:action}',
+ 'params' => array('action' => 'view')
+ ));
+ $result = $result->export();
+
+ $expected = array(
+ 'template' => '/{:controller}/{:action}',
+ 'pattern' => '@^(?:/(?P<controller>[^\\/]+))(?:/(?P<action>[^\\/]+)?)?$@',
+ 'params' => array('action' => 'view'),
+ 'defaults' => array('action' => 'view'),
+ 'match' => array(),
+ 'keys' => array('controller' => 'controller', 'action' => 'action'),
+ 'subPatterns' => array()
+ );
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/http/RouterTest.php b/libraries/lithium/tests/cases/http/RouterTest.php
new file mode 100644
index 0000000..cfb1a6c
--- /dev/null
+++ b/libraries/lithium/tests/cases/http/RouterTest.php
@@ -0,0 +1,235 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\http;
+
+use \lithium\http\Route;
+use \lithium\http\Router;
+use \lithium\action\Request;
+
+class RouterTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->request = new Request();
+ Router::connect(null);
+ }
+
+ public function testBasicRouteConnection() {
+ $result = Router::connect('/hello', array('controller' => 'posts', 'action' => 'index'));
+ $expected = array(
+ 'template' => '/hello',
+ 'pattern' => '@^/hello$@',
+ 'params' => array('controller' => 'posts', 'action' => 'index'),
+ 'match' => array('controller' => 'posts', 'action' => 'index'),
+ 'defaults' => array(),
+ 'keys' => array(),
+ 'subPatterns' => array()
+ );
+ $this->assertEqual($expected, $result->export());
+
+ $result = Router::connect('/{:controller}/{:action}', array('action' => 'view'));
+ $this->assertTrue($result instanceof Route);
+ $expected = array(
+ 'template' => '/{:controller}/{:action}',
+ 'pattern' => '@^(?:/(?P<controller>[^\\/]+))(?:/(?P<action>[^\\/]+)?)?$@',
+ 'params' => array('action' => 'view'),
+ 'defaults' => array('action' => 'view'),
+ 'match' => array(),
+ 'keys' => array('controller' => 'controller', 'action' => 'action'),
+ 'subPatterns' => array()
+ );
+ $this->assertEqual($expected, $result->export());
+ }
+
+ /**
+ * Tests generating routes with required parameters which are not present in the URL.
+ *
+ * @return void
+ */
+ public function testConnectingWithRequiredParams() {
+ $result = Router::connect('/{:controller}/{:action}', array(
+ 'action' => 'view', 'required' => true
+ ));
+ $expected = array(
+ 'template' => '/{:controller}/{:action}',
+ 'pattern' => '@^(?:/(?P<controller>[^\\/]+))(?:/(?P<action>[^\\/]+)?)?$@',
+ 'keys' => array('controller' => 'controller', 'action' => 'action'),
+ 'params' => array('action' => 'view', 'required' => true),
+ 'defaults' => array('action' => 'view'),
+ 'match' => array('required' => true),
+ 'subPatterns' => array()
+ );
+ $this->assertEqual($expected, $result->export());
+ }
+
+ public function testConnectingWithDefaultParams() {
+ $result = Router::connect('/{:controller}/{:action}', array('action' => 'archive'));
+ $expected = array(
+ 'template' => '/{:controller}/{:action}',
+ 'pattern' => '@^(?:/(?P<controller>[^\/]+))(?:/(?P<action>[^\/]+)?)?$@',
+ 'keys' => array('controller' => 'controller', 'action' => 'action'),
+ 'params' => array('action' => 'archive'),
+ 'match' => array(),
+ 'defaults' => array('action' => 'archive'),
+ 'subPatterns' => array()
+ );
+ $this->assertEqual($expected, $result->export());
+ }
+
+ /**
+ * Tests basic options for connecting routes.
+ *
+ * @return void
+ */
+ public function testBasicRouteMatching() {
+ Router::connect('/hello', array('controller' => 'posts', 'action' => 'index'));
+ $expected = array('controller' => 'posts', 'action' => 'index');
+
+ foreach (array('/hello/', '/hello', 'hello/', 'hello') as $url) {
+ $this->request->url = $url;
+ $result = Router::parse($this->request);
+ $this->assertEqual($expected, $result);
+ }
+ }
+
+ public function testRouteMatchingWithDefaultParameters() {
+ Router::connect('/{:controller}/{:action}', array('action' => 'view'));
+ $expected = array('controller' => 'posts', 'action' => 'view');
+
+ 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);
+ }
+ $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->request->url = '/posts/view/1';
+ $result = Router::parse($this->request);
+ $this->assertNull($result);
+ }
+
+ /**
+ * Tests that routing is fully reset when `Router::connect()` is passed a null value
+ *
+ * @return void
+ */
+ public function testResettingRoutes() {
+ Router::connect('/{:controller}', array('controller' => 'posts'));
+ $this->request->url = '/hello';
+
+ $expected = array('controller' => 'hello', 'action' => 'index');
+ $result = Router::parse($this->request);
+ $this->assertEqual($expected, $result);
+
+ Router::connect(null);
+ $this->assertNull(Router::parse($this->request));
+ }
+
+ /**
+ * Tests matching routes where the route template is a static string with no insert parameters.
+ *
+ * @return void
+ */
+ public function testRouteMatchingWithNoInserts() {
+ 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')));
+ }
+
+ /**
+ * Test matching routes with only insert parameters and no default values.
+ *
+ * @return void
+ */
+ public function testRouteMatchingWithOnlyInserts() {
+ Router::connect('/{:controller}');
+ $this->assertEqual('/posts', Router::match(array('controller' => 'posts')));
+
+ $result = Router::match(array('controller' => 'posts', 'action' => 'view'));
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Test matching routes with insert parameters which have default values.
+ *
+ * @return void
+ */
+ public function testRouteMatchingWithInsertsAndDefaults() {
+ Router::connect('/{:controller}/{:action}', array('action' => 'archive'));
+ $this->assertEqual('/posts', Router::match(array('controller' => 'posts')));
+
+ $result = Router::match(array('controller' => 'posts', 'action' => 'archive'));
+ $this->assertEqual('/posts/archive', $result);
+
+ Router::connect(null);
+ Router::connect('/{:controller}/{:action}', array('controller' => 'users'));
+
+ $result = Router::match(array('action' => 'view'));
+ $this->assertEqual('/users/view', $result);
+
+ $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);
+ }
+
+ /**
+ * Tests getting routes using `Router::get()`, and checking to see if the routes returned match
+ * the routes connected.
+ *
+ * @return void
+ */
+ public function testRouteRetrieval() {
+ $expected = Router::connect('/hello', array('controller' => 'posts', 'action' => 'index'));
+ $result = Router::get(0);
+ $this->assertIdentical($expected, $result);
+
+ list($result) = Router::get();
+ $this->assertIdentical($expected, $result);
+ }
+
+ public function testStringUrlGeneration() {
+ $result = Router::match('/posts');
+ $expected = '/posts';
+ $this->assertEqual($expected, $result);
+
+ $result = Router::match('/posts');
+ $this->assertEqual($expected, $result);
+
+ $result = Router::match('/posts/view/5');
+ $expected = '/posts/view/5';
+ $this->assertEqual($expected, $result);
+
+ $request = new Request(array('base' => '/my/web/path'));
+ $result = Router::match('/posts', $request);
+ $expected = '/my/web/path/posts';
+ $this->assertEqual($expected, $result);
+
+ $result = Router::match('mailto:foo@localhost');
+ $expected = 'mailto:foo@localhost';
+ $this->assertEqual($expected, $result);
+
+ $result = Router::match('#top');
+ $expected = '#top';
+ $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
new file mode 100644
index 0000000..5b98300
--- /dev/null
+++ b/libraries/lithium/tests/cases/storage/CacheTest.php
@@ -0,0 +1,255 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, 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;
+
+use \lithium\storage\Cache;
+use \lithium\util\Collection;
+
+class CacheTest extends \lithium\test\Unit {
+
+ public function tearDown() {
+ Cache::reset();
+ }
+
+ public function testBasicCacheConfig() {
+ $result = Cache::config();
+ $this->assertEqual(new Collection(), $result);
+
+ $config = array('default' => array('adapter' => '\some\adapter', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testkeyNoContext() {
+ $key = 'this is a cache key';
+
+ $result = Cache::key($key);
+ $expected = 'this_is_a_cache_key';
+ $this->assertIdentical($expected, $result);
+
+ $key = '1120-cache éë';
+
+ $result = Cache::key($key);
+ $expected = '1120_cache_ee';
+ $this->assertIdentical($expected, $result);
+ }
+
+ public function testKeyWithLambda() {
+ $key = function() {
+ return 'lambda_key';
+ };
+
+ $result = Cache::key($key);
+ $expected = 'lambda_key';
+ $this->assertIdentical($expected, $result);
+
+ $key = function() {
+ return 'lambda key';
+ };
+
+ $result = Cache::key($key);
+ $expected = 'lambda_key';
+ $this->assertIdentical($expected, $result);
+
+ $key = function($data = array()) {
+ $defaults = array('foo' => 'foo', 'bar' => 'bar');
+ $data += $defaults;
+ return 'composed_key_with_' . $data['foo'] . '_' . $data['bar'];
+ };
+
+ $result = Cache::key($key, array('foo' => 'boo', 'bar' => 'far'));
+ $expected = 'composed_key_with_boo_far';
+ $this->assertIdentical($expected, $result);
+ }
+
+ public function testKeyWithClosure() {
+ $value = 5;
+
+ $key = function() use ($value) {
+ return "closure key {$value}";
+ };
+
+ $result = Cache::key($key);
+ $expected = 'closure_key_5';
+ $this->assertIdentical($expected, $result);
+
+ $reference = 'mutable';
+
+ $key = function () use (&$reference) {
+ $reference .= ' key';
+ return $reference;
+ };
+
+ $result = Cache::key($key);
+ $expected = 'mutable_key';
+ $this->assertIdentical($expected, $result);
+ $this->assertIdentical('mutable key', $reference);
+ }
+
+ public function testKeyWithClosureAndArguments() {
+ $value = 'closure argument';
+
+ $key = function($value) {
+ return $value;
+ };
+
+ $result = Cache::key($key($value));
+ $expected = 'closure_argument';
+ $this->assertIdentical($expected, $result);
+ }
+
+ public function testCacheWrite() {
+ $config = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::write('default', 'some_key', 'some_data', '+1 minute');
+ $this->assertTrue($result);
+
+ $result = Cache::write('non_existing', 'key_value', 'data', '+1 minute');
+ $this->assertFalse($result);
+ }
+
+ public function testCacheReadAndWrite() {
+ $config = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::read('non_existing', 'key_value', 'data');
+ $this->assertFalse($result);
+
+ $result = Cache::write('default', 'keyed', 'some data', '+1 minute');
+ $this->assertTrue($result);
+
+ $result = Cache::read('default', 'keyed');
+ $expected = 'some data';
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::write('default', 'another', array('data' => 'take two'), '+1 minute');
+ $this->assertTrue($result);
+
+ $result = Cache::read('default', 'another');
+ $expected = array('data' => 'take two');
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::write('default', 'another', (object) array('data' => 'take two'), '+1 minute');
+ $this->assertTrue($result);
+
+ $result = Cache::read('default', 'another');
+ $expected = (object) array('data' => 'take two');
+ $this->assertEqual($expected, $result);
+
+ }
+
+ public function testCacheWriteAndDelete() {
+ $config = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::delete('non_existing', 'key_value', 'data');
+ $this->assertFalse($result);
+
+ $result = Cache::write('default', 'to delete', 'dead data', '+1 minute');
+ $this->assertTrue($result);
+
+ $result = Cache::delete('default', 'to delete');
+ $this->assertTrue($result);
+ $this->assertFalse(Cache::read('default', 'to delete'));
+ }
+
+ public function testCacheWriteAndClear() {
+ $config = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::clear('non_existing');
+ $this->assertFalse($result);
+
+ $result = Cache::write('default', 'to delete', 'dead data', '+1 minute');
+ $this->assertTrue($result);
+
+ $result = Cache::clear('default');
+ $this->assertTrue($result);
+
+ $result = Cache::read('default', 'to delete');
+ $this->assertFalse($result);
+
+ }
+
+ public function testClean() {
+ $config = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::clean('non_existing');
+ $this->assertFalse($result);
+
+ $result = Cache::clean('default');
+ $this->assertFalse($result);
+
+ }
+
+ public function testReset() {
+ $config = array('default' => array('adapter' => 'Memory', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+
+ $result = Cache::reset();
+ $this->assertNull($result);
+
+ $result = Cache::config();
+ $expected = new Collection();
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testIntegrationFileAdapterCacheConfig() {
+ $result = Cache::config();
+ $this->assertEqual(new Collection(), $result);
+
+ $config = array('default' => array('adapter' => 'File', 'filters' => array()));
+ $result = Cache::config($config);
+ $expected = new Collection(array('items' => $config));
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testIntegrationFileAdapterWrite() {
+ $config = array('default' => array(
+ 'adapter' => 'File',
+ 'path' => LITHIUM_APP_PATH . '/tmp/cache',
+ 'filters' => array()
+ ));
+ Cache::config($config);
+
+ $result = Cache::write('default', 'key', 'value', '+1 minute');
+ $this->assertTrue($result);
+
+ $time = time() + 60;
+ $result = file_get_contents(LITHIUM_APP_PATH . '/tmp/cache/key');
+ $expected = "{:expiry:$time}\nvalue";
+ $this->assertEqual($result, $expected);
+
+ $result = unlink(LITHIUM_APP_PATH . '/tmp/cache/key');
+ $this->assertTrue($result);
+ $this->assertFalse(file_exists(LITHIUM_APP_PATH . '/tmp/cache/key'));
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/SessionTest.php b/libraries/lithium/tests/cases/storage/SessionTest.php
new file mode 100644
index 0000000..7401730
--- /dev/null
+++ b/libraries/lithium/tests/cases/storage/SessionTest.php
@@ -0,0 +1,180 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, 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;
+
+use \lithium\storage\Session;
+use \lithium\util\Collection;
+use \lithium\storage\session\adapters\Memory;
+
+class SessionStorageConditional extends Memory {
+
+ public function read($key, $options = array()) {
+ return isset($options['fail']) ? null : parent::read($key, $options);
+ }
+
+ public function write($key, $value, $options = array()) {
+ return isset($options['fail']) ? null : parent::write($key, $value, $options);
+ }
+}
+
+class SessionTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Session::config(array(
+ 'default' => array('adapter' => new Memory())
+ ));
+ }
+
+ public function testSessionInitialization() {
+ $store1 = new Memory();
+ $store2 = new Memory();
+ $config = array(
+ 'store1' => array('adapter' => &$store1, 'filters' => array()),
+ 'store1' => array('adapter' => &$store1, 'filters' => array())
+ );
+
+ $result = Session::config($config);
+ $this->assertEqual(new Collection(array('items' => $config)), $result);
+
+ Session::clear();
+ Session::config(array('store1' => array(
+ 'adapter' => 'lithium\storage\session\adapters\Memory',
+ 'filters' => array()
+ )));
+ $this->assertTrue(Session::write('key', 'value'));
+ $result = Session::read('key');
+ $expected = 'value';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testSingleStoreReadWrite() {
+ $this->assertNull(Session::read('key'));
+
+ $this->assertTrue(Session::write('key', 'value'));
+ $this->assertEqual(Session::read('key'), 'value');
+
+ Session::clear();
+ $this->assertNull(Session::read('key'));
+ $this->assertIdentical(false, Session::write('key', 'value'));
+ }
+
+ public function testSessionConfigReset() {
+ $this->assertTrue(Session::write('key', 'value'));
+ $this->assertEqual(Session::read('key'), 'value');
+
+ Session::clear();
+ $this->assertFalse(Session::config()->count());
+
+ $this->assertFalse(Session::read('key'));
+ $this->assertFalse(Session::write('key', 'value'));
+ }
+
+ /**
+ * Tests a scenario where no session handler is available that matches the passed parameters.
+ *
+ * @return void
+ */
+ public function testUnhandledWrite() {
+ Session::config(array(
+ 'conditional' => array('adapter' => new SessionStorageConditional())
+ ));
+ $result = Session::write('key', 'value', array('fail' => true));
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Tests deleting a session key from one or all adapters.
+ *
+ * @return void
+ */
+ public function testSessionKeyCheckAndDelete() {
+ Session::config(array(
+ 'temp' => array('adapter' => new Memory(), 'filters' => array()),
+ 'persistent' => array('adapter' => new Memory(), 'filters' => array())
+ ));
+ 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);
+
+ $result = Session::check('key1', array('name' => 'persistent'));
+ $this->assertTrue($result);
+
+ $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);
+
+ Session::delete('key1');
+ $result = Session::check('key1');
+ $this->assertFalse($result);
+
+ Session::write('key1', 'value', array('name' => 'persistent'));
+ $result = Session::check('key1');
+ $this->assertTrue($result);
+
+ Session::delete('key1', array('name' => 'temp'));
+ $result = Session::check('key1');
+ $this->assertTrue($result);
+
+ Session::delete('key1', array('name' => 'persistent'));
+ $result = Session::check('key1');
+ $this->assertFalse($result);
+ }
+
+ /**
+ * Tests querying session keys from the primary adapter. The memory adapter simply returns
+ * the HTTP request ID.
+ *
+ * @return void
+ */
+ public function testSessionKey() {
+ $this->skipIf(!isset($_SERVER['UNIQUE_ID']));
+
+ $expected = $_SERVER['UNIQUE_ID'];
+ $result = Session::key();
+ $this->assertEqual($expected, $result);
+
+ Session::clear();
+ $this->assertNull(Session::key());
+ }
+
+ public function testConfigNoAdapters() {
+ Session::config(array(
+ 'conditional' => array('adapter' => new SessionStorageConditional())
+ ));
+ $this->assertTrue(Session::write('key', 'value'));
+ $this->assertEqual(Session::read('key'), 'value');
+ $this->assertFalse(Session::read('key', array('fail' => true)));
+ }
+
+ public function testSessionState() {
+ $this->assertTrue(Session::isStarted());
+ $this->assertTrue(Session::isStarted('default'));
+ $this->assertFalse(Session::isStarted('invalid'));
+
+ Session::clear();
+ $this->assertFalse(Session::isStarted());
+ $this->assertFalse(Session::isStarted('default'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/cache/adapters/ApcTest.php b/libraries/lithium/tests/cases/storage/cache/adapters/ApcTest.php
new file mode 100644
index 0000000..5f8a9d7
--- /dev/null
+++ b/libraries/lithium/tests/cases/storage/cache/adapters/ApcTest.php
@@ -0,0 +1,263 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, 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\cache\adapters;
+
+use \lithium\storage\cache\adapters\Apc;
+
+class ApcTest extends \lithium\test\Unit {
+
+ /**
+ * Skip the test if APC extension is unavailable.
+ *
+ * @return void
+ */
+ public function skip() {
+ $extensionExists = extension_loaded('apc');
+ $message = 'The apc extension is not installed.';
+ $this->skipIf(!$extensionExists, $message);
+ }
+
+ public function setUp() {
+ apc_clear_cache('user');
+ $this->Apc = new Apc();
+ }
+
+ public function tearDown() {
+ apc_clear_cache('user');
+ unset($this->Apc);
+ }
+
+ public function testSimpleWrite() {
+ $key = 'key';
+ $data = 'value';
+ $expiry = '+5 seconds';
+ $time = strtotime($expiry);
+
+ $closure = $this->Apc->write($key, $data, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->Apc, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = apc_fetch($key);
+ $this->assertEqual($expected, $result);
+
+ $result = apc_fetch($key . '_expires');
+ $this->assertEqual($time, $result);
+
+ $result = apc_delete($key);
+ $this->assertTrue($result);
+
+ $key = 'another_key';
+ $data = 'more_data';
+ $expiry = '+1 minute';
+ $time = strtotime($expiry);
+
+ $closure = $this->Apc->write($key, $data, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->Apc, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = apc_fetch($key);
+ $this->assertEqual($expected, $result);
+
+ $result = apc_fetch($key . '_expires');
+ $this->assertEqual($time, $result);
+
+ $result = apc_delete($key);
+ $this->assertTrue($result);
+
+ $result = apc_delete($key . '_expires');
+ $this->assertTrue($result);
+ }
+
+ public function testSimpleRead() {
+ $key = 'read_key';
+ $data = 'read data';
+ $time = strtotime('+1 minute');
+
+ $result = apc_store($key . '_expires', $time, 60);
+ $this->assertTrue($result);
+
+ $result = apc_store($key, $data, 60);
+ $this->assertTrue($result);
+
+ $closure = $this->Apc->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = apc_delete($key);
+ $this->assertTrue($result);
+
+ $result = apc_delete($key . '_expires');
+ $this->assertTrue($result);
+
+ $key = 'another_read_key';
+ $data = 'read data';
+ $time = strtotime('+1 minute');
+
+ $result = apc_store($key, $data, 60);
+ $this->assertTrue($result);
+
+ $result = apc_store($key . '_expires', $time, 60);
+ $this->assertTrue($result);
+
+ $closure = $this->Apc->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = apc_delete($key);
+ $this->assertTrue($result);
+
+ $result = apc_delete($key . '_expires');
+ $this->assertTrue($result);
+ }
+
+ public function testReadKeyThatDoesNotExist() {
+ $key = 'does_not_exist';
+ $closure = $this->Apc->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $this->assertFalse($result);
+
+ }
+
+ public function testDelete() {
+ $key = 'delete_key';
+ $data = 'data to delete';
+ $time = strtotime('+1 minute');
+
+ $result = apc_store($key, $data, 60);
+ $this->assertTrue($result);
+
+ $result = apc_store($key . '_expires', $time, 60);
+ $this->assertTrue($result);
+
+ $closure = $this->Apc->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $this->assertTrue($result);
+
+ $this->assertFalse(apc_delete($key));
+ $this->assertFalse(apc_delete($key . '_expires'));
+ }
+
+ public function testDeleteNonExistentKey() {
+ $key = 'delete_key';
+ $data = 'data to delete';
+ $time = strtotime('+1 minute');
+
+ $closure = $this->Apc->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $this->assertFalse($result);
+ }
+
+ public function testWriteReadAndDeleteRoundtrip() {
+ $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));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->Apc, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = apc_fetch($key);
+ $this->assertEqual($expected, $result);
+
+ $result = apc_fetch($key . '_expires');
+ $this->assertEqual($time, $result);
+
+ $closure = $this->Apc->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $closure = $this->Apc->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $this->assertTrue($result);
+
+ $this->assertFalse(apc_fetch($key));
+ $this->assertFalse(apc_fetch($key . '_expires'));
+ }
+
+ public function testExpiredRead() {
+ $key = 'expiring_read_key';
+ $data = 'expired data';
+ $time = strtotime('+1 second');
+
+ $result = apc_store($key . '_expires', $time, 1);
+ $this->assertTrue($result);
+
+ $result = apc_store($key, $data, 1);
+ $this->assertTrue($result);
+
+ sleep(2);
+ $closure = $this->Apc->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Apc, $params, null);
+ $this->assertFalse($result);
+ }
+
+ 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);
+ $this->assertTrue($result);
+
+ $result = apc_store($key2, 'more dead data', $time);
+ $this->assertTrue($result);
+
+ $result = $this->Apc->clear();
+ $this->assertTrue($result);
+
+ $this->assertFalse(apc_fetch($key1));
+ $this->assertFalse(apc_fetch($key2));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/cache/adapters/FileTest.php b/libraries/lithium/tests/cases/storage/cache/adapters/FileTest.php
new file mode 100644
index 0000000..8cedd2f
--- /dev/null
+++ b/libraries/lithium/tests/cases/storage/cache/adapters/FileTest.php
@@ -0,0 +1,126 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, 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\cache\adapters;
+
+use \lithium\storage\cache\adapters\File;
+
+class FileTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->File = new File();
+ }
+
+ public function tearDown() {
+ unset($this->File);
+ }
+
+ public function testWrite() {
+ $key = 'key';
+ $data = 'data';
+ $expiry = '+1 minute';
+ $time = time() + 60;
+
+ $closure = $this->File->write($key, $data, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->File, $params, null);
+ $expected = 25;
+ $this->assertEqual($expected, $result);
+
+ $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+ $this->assertEqual(file_get_contents(LITHIUM_APP_PATH . "/tmp/cache/$key"), "{:expiry:$time}\ndata");
+
+ $this->assertTrue(unlink(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+ $this->assertFalse(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+ }
+
+ public function testRead() {
+ $key = 'key';
+ $time = time() + 60;
+
+ $closure = $this->File->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ file_put_contents(LITHIUM_APP_PATH . "/tmp/cache/$key", "{:expiry:$time}\ndata");
+ $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+
+ $params = compact('key');
+ $result = $closure($this->File, $params, null);
+ $expected = 'data';
+ $this->assertEqual($expected, $result);
+
+ $this->assertTrue(unlink(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+ $this->assertFalse(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+
+ $key = 'non_existent';
+ $params = compact('key');
+ $closure = $this->File->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $result = $closure($this->File, $params, null);
+ $this->assertFalse($result);
+ }
+
+ public function testExpiredRead() {
+ $key = 'expired_key';
+ $time = time() + 1;
+
+ $closure = $this->File->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ file_put_contents(LITHIUM_APP_PATH . "/tmp/cache/$key", "{:expiry:$time}\ndata");
+ $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+
+ sleep(2);
+ $params = compact('key');
+ $result = $closure($this->File, $params, null);
+ $this->assertFalse($result);
+
+ }
+
+ public function testDelete() {
+ $key = 'key_to_delete';
+ $time = time() + 1;
+
+ file_put_contents(LITHIUM_APP_PATH . "/tmp/cache/$key", "{:expiry:$time}\ndata");
+ $this->assertTrue(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+
+ $closure = $this->File->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->File, $params, null);
+ $this->assertTrue($result);
+
+ $key = 'non_existent';
+ $params = compact('key');
+ $result = $closure($this->File, $params, null);
+ $this->assertFalse($result);
+ }
+
+ public function testClear() {
+ $key = 'key_to_clear';
+ $time = time() + 1;
+ file_put_contents(LITHIUM_APP_PATH . "/tmp/cache/$key", "{:expiry:$time}\ndata");
+
+ $result = $this->File->clear();
+ $this->assertTrue($result);
+ $this->assertFalse(file_exists(LITHIUM_APP_PATH . "/tmp/cache/$key"));
+
+ }
+
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/storage/cache/adapters/MemcacheTest.php b/libraries/lithium/tests/cases/storage/cache/adapters/MemcacheTest.php
new file mode 100644
index 0000000..9a25a1a
--- /dev/null
+++ b/libraries/lithium/tests/cases/storage/cache/adapters/MemcacheTest.php
@@ -0,0 +1,265 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, 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\cache\adapters;
+
+use \lithium\storage\cache\adapters\Memcache;
+
+class MemcacheTest extends \lithium\test\Unit {
+
+ /**
+ * Skip the test if Memcached extension is unavailable.
+ *
+ * @return void
+ */
+ public function skip() {
+ $extensionExists = extension_loaded('memcached');
+ $message = 'The libmemcached extension is not installed.';
+ $this->skipIf(!$extensionExists, $message);
+ }
+
+ public function setUp() {
+ $this->server = array('host' => '127.0.0.1', 'port' => 11211, 'weight' => 100);
+
+
+ $this->_Memcached = new \Memcached();
+ $this->_Memcached->addServer($this->server['host'], $this->server['port'], $this->server['weight']);
+
+ $this->Memcache = new Memcache();
+ }
+
+ public function tearDown() {
+ $this->_Memcached->flush();
+ }
+
+ public function testSimpleWrite() {
+ $key = 'key';
+ $data = 'value';
+ $expiry = '+5 seconds';
+ $time = strtotime($expiry);
+
+ $closure = $this->Memcache->write($key, $data, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key . '_expires');
+ $this->assertEqual($time, $result);
+
+ $result = $this->_Memcached->delete($key);
+ $this->assertTrue($result);
+
+ $key = 'another_key';
+ $data = 'more_data';
+ $expiry = '+1 minute';
+ $time = strtotime($expiry);
+
+ $closure = $this->Memcache->write($key, $data, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key . '_expires');
+ $this->assertEqual($time, $result);
+
+ $result = $this->_Memcached->delete($key);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->delete($key . '_expires');
+ $this->assertTrue($result);
+ }
+
+ public function testSimpleRead() {
+ $key = 'read_key';
+ $data = 'read data';
+ $time = strtotime('+1 minute');
+
+ $result = $this->_Memcached->set($key . '_expires', $time, $time);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->set($key, $data, $time);
+ $this->assertTrue($result);
+
+ $closure = $this->Memcache->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->delete($key);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->delete($key . '_expires');
+ $this->assertTrue($result);
+
+ $key = 'another_read_key';
+ $data = 'read data';
+ $time = strtotime('+1 minute');
+
+ $result = $this->_Memcached->set($key, $data, $time);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->set($key . '_expires', $time, $time);
+ $this->assertTrue($result);
+
+ $closure = $this->Memcache->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->delete($key);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->delete($key . '_expires');
+ $this->assertTrue($result);
+ }
+
+ public function testReadKeyThatDoesNotExist() {
+ $key = 'does_not_exist';
+ $closure = $this->Memcache->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $this->assertFalse($result);
+
+ }
+
+ public function testDelete() {
+ $key = 'delete_key';
+ $data = 'data to delete';
+ $time = strtotime('+1 minute');
+
+ $result = $this->_Memcached->set($key, $data, $time);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->set($key . '_expires', $time, $time);
+ $this->assertTrue($result);
+
+ $closure = $this->Memcache->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $this->assertTrue($result);
+
+ $this->assertFalse($this->_Memcached->delete($key));
+ $this->assertFalse($this->_Memcached->delete($key . '_expires'));
+ }
+
+ public function testDeleteNonExistentKey() {
+ $key = 'delete_key';
+ $data = 'data to delete';
+ $time = strtotime('+1 minute');
+
+ $closure = $this->Memcache->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $this->assertFalse($result);
+ }
+
+ public function testWriteReadAndDeleteRoundtrip() {
+ $key = 'write_read_key';
+ $data = 'write/read value';
+ $expiry = '+5 seconds';
+ $time = strtotime($expiry);
+
+ $closure = $this->Memcache->write($key, $data, $expiry);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key', 'data', 'expiry');
+ $result = $closure($this->Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->_Memcached->get($key . '_expires');
+ $this->assertEqual($time, $result);
+
+ $closure = $this->Memcache->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $expected = $data;
+ $this->assertEqual($expected, $result);
+
+ $closure = $this->Memcache->delete($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $this->assertTrue($result);
+
+ $this->assertFalse($this->_Memcached->get($key));
+ $this->assertFalse($this->_Memcached->get($key . '_expires'));
+ }
+
+ public function testExpiredRead() {
+ $key = 'expiring_read_key';
+ $data = 'expired data';
+ $time = strtotime('+1 second');
+
+ $result = $this->_Memcached->set($key . '_expires', $time, $time);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->set($key, $data, $time);
+ $this->assertTrue($result);
+
+ sleep(2);
+ $closure = $this->Memcache->read($key);
+ $this->assertTrue(is_callable($closure));
+
+ $params = compact('key');
+ $result = $closure($this->Memcache, $params, null);
+ $this->assertFalse($result);
+ }
+
+ public function testClear() {
+ $time = strtotime('+1 minute');
+
+ $result = $this->_Memcached->set('key', 'value', $time);
+ $this->assertTrue($result);
+
+ $result = $this->_Memcached->set('another_key', 'value', $time);
+ $this->assertTrue($result);
+
+ $result = $this->Memcache->clear();
+ $this->assertTrue($result);
+
+ $this->assertFalse($this->_Memcached->get('key'));
+ $this->assertFalse($this->_Memcached->get('another_key'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/HelperTest.php b/libraries/lithium/tests/cases/template/HelperTest.php
new file mode 100644
index 0000000..aa60b3d
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/HelperTest.php
@@ -0,0 +1,88 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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\template\view\Renderer;
+
+class MyHelper extends Helper {
+
+ /**
+ * Hack to expose protected properties for testing.
+ *
+ * @param string $property
+ * @return mixed
+ */
+ public function __get($property) {
+ return isset($this->{$property}) ? $this->{$property} : null;
+ }
+}
+
+class MyRenderer extends Renderer {}
+
+class HelperTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->helper = new MyHelper();
+ }
+
+ /**
+ * Tests that constructor parameters are properly assigned to protected properties.
+ *
+ * @return void
+ */
+ public function testObjectConstructionWithParameters() {
+ $this->assertNull($this->helper->_context);
+
+ $params = array(
+ 'context' => new MyRenderer(),
+ 'handlers' => array('content' => function($value) { return "\n{$value}\n"; })
+ );
+ $this->helper = new MyHelper($params);
+
+ $this->assertEqual($this->helper->_context, $params['context']);
+ }
+
+ /**
+ * Tests the default escaping for HTML output. When implementing helpers that do not output
+ * HTML/XML, the `escape()` method should be overridden accordingly.
+ *
+ * @return void
+ */
+ public function testDefaultEscaping() {
+ $result = $this->helper->escape('<script>alert("XSS!");</script>');
+ $expected = '<script>alert("XSS!");</script>';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->helper->escape('<script>//alert("XSS!");</script>', null, array(
+ 'escape' => false
+ ));
+ $expected = '<script>//alert("XSS!");</script>';
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests unescaped values passed through the escape() method. Unescaped values
+ * should be returned exactly the same as the original value.
+ *
+ * @return void
+ */
+ public function testUnescapedValue() {
+ $value = '<blockquote>"Thou shalt not escape!"</blockquote>';
+ $result = $this->helper->escape($value, null, array('escape' => false));
+ $this->assertEqual($value, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/ViewTest.php b/libraries/lithium/tests/cases/template/ViewTest.php
new file mode 100644
index 0000000..f78ab02
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/ViewTest.php
@@ -0,0 +1,37 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class ViewTest extends \lithium\test\Unit {
+
+ protected $_view = null;
+
+ public function setUp() {
+ $this->_view = new View();
+ }
+
+ public function testInitialization() {
+ $this->_view = new View();
+ }
+
+ public function testInitializationWithBadClasses() {
+ $this->expectException('Template adapter Badness not found');
+ new View(array('loader' => 'Badness'));
+ $this->expectException('Template adapter Badness not found');
+ new View(array('renderer' => 'Badness'));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/helpers/FormTest.php b/libraries/lithium/tests/cases/template/helpers/FormTest.php
new file mode 100644
index 0000000..e37c282
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/helpers/FormTest.php
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\template\helpers;
+
+use \lithium\http\Router;
+use \lithium\template\helpers\Form;
+use \lithium\template\view\Renderer;
+
+class MyFormRenderer extends Renderer {}
+
+class FormTest extends \lithium\test\Unit {
+
+ /**
+ * Test object instance
+ *
+ * @var object
+ */
+ public $form = null;
+
+ protected $_routes = array();
+
+ /**
+ * Initialize test by creating a new object instance with a default context.
+ *
+ * @return void
+ */
+ public function setUp() {
+ $this->_routes = Router::get();
+ Router::connect(null);
+ Router::connect('/{:controller}/{:action}/{:id}.{:type}');
+ Router::connect('/{:controller}/{:action}.{:type}');
+
+ $this->form = new Form(array('context' => new MyFormRenderer()));
+ }
+
+ public function testTextBox() {
+ $result = $this->form->text('foo');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'text', 'name' => 'foo'
+ )));
+ }
+
+ public function testElementsWithDefaultConfiguration() {
+ $this->form = new Form(array(
+ 'context' => new MyFormRenderer(), 'base' => array('class' => 'editable')
+ ));
+
+ $result = $this->form->text('foo');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'text', 'name' => 'foo', 'class' => 'editable'
+ )));
+
+ $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'
+ )));
+
+ $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'
+ )));
+
+ $result = $this->form->config();
+ $expected = array(
+ 'base' => array('class' => 'editable', 'maxlength' => 255),
+ 'text' => array('class' => 'locked'),
+ 'textarea' => array(),
+ 'templates' => array()
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ 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'
+ )));
+
+ $result = $this->form->text('foo', array(
+ 'default' => 'Message here', 'value' => 'My Name Is Jonas'
+ ));
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'text', 'name' => 'foo', 'value' => 'My Name Is Jonas'
+ )));
+
+ $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'
+ )));
+ }
+
+ public function testLabelGeneration() {
+ $result = $this->form->label('next', 'Enter the next value >>');
+ $this->assertTags($result, array(
+ 'label' => array('for' => 'next'),
+ 'Enter the next value >>',
+ '/label'
+ ));
+ }
+
+ public function testTemplateRemapping() {
+ $result = $this->form->password('passwd');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'password', 'name' => 'passwd'
+ )));
+
+ $this->form->config(array('templates' => array('password' => 'text')));
+
+ $result = $this->form->password('passwd');
+ $this->assertTags($result, array('input' => array(
+ 'type' => 'text', 'name' => 'passwd'
+ )));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/helpers/HtmlTest.php b/libraries/lithium/tests/cases/template/helpers/HtmlTest.php
new file mode 100644
index 0000000..b068fda
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/helpers/HtmlTest.php
@@ -0,0 +1,509 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\template\helpers;
+
+use \lithium\http\Router;
+use \lithium\template\helpers\Html;
+use \lithium\template\view\Renderer;
+
+class MyHtmlRenderer extends Renderer {}
+
+class HtmlTest extends \lithium\test\Unit {
+
+ /**
+ * Test object instance
+ *
+ * @var object
+ */
+ public $html = null;
+
+ protected $_routes = array();
+
+ /**
+ * Initialize test by creating a new object instance with a default context.
+ *
+ * @return void
+ */
+ public function setUp() {
+ $this->_routes = Router::get();
+ Router::connect(null);
+ Router::connect('/{:controller}/{:action}/{:id}.{:type}');
+ Router::connect('/{:controller}/{:action}.{:type}');
+
+ $this->html = new Html(array('context' => new MyHtmlRenderer()));
+ }
+
+ /**
+ * Clean up after the test.
+ *
+ * @return void
+ */
+ public function tearDown() {
+ Router::connect(null);
+ $this->_routes->each(function($route) { Router::connect($route); });
+ unset($this->html);
+ }
+
+ /**
+ * testDocType method
+ *
+ * @access public
+ * @return void
+ */
+ function testDocType() {
+ $result = $this->html->docType('xhtml-strict');
+ $expected = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" ';
+ $expected .= '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
+
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->docType('html4-strict');
+ $expected = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" ';
+ $expected .= '"http://www.w3.org/TR/html4/strict.dtd">';
+ $this->assertEqual($result, $expected);
+
+ $this->assertNull($this->html->docType('badness'));
+ }
+
+ /**
+ * Tests that character set declarations render the correct character set and meta tag.
+ *
+ * @return void
+ */
+ public function testCharset() {
+ $result = $this->html->charset();
+
+ $this->assertTags($result, array('meta' => array(
+ 'http-equiv' => 'Content-Type', 'content' => 'text/html; 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'
+ )));
+ }
+
+ /**
+ * Tests meta linking.
+ *
+ * @return void
+ */
+ public function testMetaLink() {
+ $result = $this->html->link(
+ 'RSS Feed',
+ array('controller' => 'posts', 'type' => 'rss'),
+ array('type' => 'rss')
+ );
+
+ $this->assertTags($result, array('link' => array(
+ 'href' => 'preg:/.*\/posts\/index\.rss/',
+ 'type' => 'application/rss+xml',
+ 'rel' => 'alternate',
+ 'title' => 'RSS Feed'
+ )));
+
+ $result = $this->html->link(
+ 'Atom Feed', array('controller' => 'posts', 'type' => 'xml'), array('type' => 'atom')
+ );
+ $this->assertTags($result, array('link' => array(
+ 'href' => 'preg:/.*\/posts\/index\.xml/',
+ 'type' => 'application/atom+xml',
+ 'title' => 'Atom Feed',
+ 'rel' => 'alternate'
+ )));
+
+ $result = $this->html->link('No-existy', '/posts.xmp', array('type' => 'rong'));
+ $this->assertTags($result, array('link' => array(
+ 'href' => 'preg:/.*\/posts\.xmp/',
+ 'title' => 'No-existy',
+ )));
+
+ $result = $this->html->link('No-existy', '/posts.xpp', array('type' => 'atom'));
+ $this->assertTags($result, array('link' => array(
+ 'href' => 'preg:/.*\/posts\.xpp/',
+ 'type' => 'application/atom+xml',
+ 'title' => 'No-existy',
+ 'rel' => 'alternate'
+ )));
+
+ $result = $this->html->link('Favicon', array(), array('type' => 'icon'));
+ $expected = array(
+ 'link' => array(
+ 'href' => 'preg:/.*favicon\.ico/',
+ 'type' => 'image/x-icon',
+ 'rel' => 'icon',
+ 'title' => 'Favicon'
+ ),
+ array('link' => array(
+ 'href' => 'preg:/.*favicon\.ico/',
+ 'type' => 'image/x-icon',
+ 'rel' => 'shortcut icon',
+ 'title' => 'Favicon'
+ ))
+ );
+ $this->assertTags($result, $expected);
+ }
+
+ /**
+ * Tests <a /> elements generated by `HtmlHelper::link()`
+ *
+ * @return void
+ */
+ public function testLink() {
+ $result = $this->html->link('/home');
+ $expected = array('a' => array('href' => '/home'), 'preg:/\/home/', '/a');
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->link('Next >', '#');
+ $expected = array('a' => array('href' => '#'), 'Next >', '/a');
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->link('Next >', '#', array('escape' => true));
+ $expected = array(
+ 'a' => array('href' => '#'),
+ 'Next >',
+ '/a'
+ );
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->link('Next >', '#', array('escape' => 'utf-8'));
+ $expected = array(
+ 'a' => array('href' => '#'),
+ 'Next >',
+ '/a'
+ );
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->link('Next >', '#', array('escape' => false));
+ $expected = array('a' => array('href' => '#'), 'Next >', '/a');
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->link('Next >', '#', array(
+ 'title' => 'to escape … or not escape?',
+ 'escape' => false
+ ));
+ $expected = array(
+ 'a' => array('href' => '#', 'title' => 'to escape … or not escape?'),
+ 'Next >',
+ '/a'
+ );
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->link('Next >', '#', array(
+ 'title' => 'to escape … or not escape?', 'escape' => true
+ ));
+ $expected = array(
+ 'a' => array('href' => '#', 'title' => 'to escape &#8230; or not escape?'),
+ 'Next >',
+ '/a'
+ );
+ $this->assertTags($result, $expected);
+
+ // $result = $this->html->link('Original size', array(
+ // 'controller' => 'images', 'action' => 'view', 3, '?' => array(
+ // 'height' => 100, 'width' => 200
+ // )
+ // ));
+ // $expected = array(
+ // 'a' => array('href' => '/images/view/3?height=100&width=200'),
+ // 'Original size',
+ // '/a'
+ // );
+ // $this->assertTags($result, $expected);
+
+ // Configure::write('Asset.timestamp', false);
+ //
+ // $result = $this->html->link($this->html->image('test.gif'), '#', array());
+ // $expected = array(
+ // 'a' => array('href' => '#'),
+ // 'img' => array('src' => 'img/test.gif', 'alt' => ''),
+ // '/a'
+ // );
+ // $this->assertTags($result, $expected);
+ //
+ // $result = $this->html->image('test.gif', array('url' => '#'));
+ // $expected = array(
+ // 'a' => array('href' => '#'),
+ // 'img' => array('src' => 'img/test.gif', 'alt' => ''),
+ // '/a'
+ // );
+ // $this->assertTags($result, $expected);
+ //
+ // Configure::write('Asset.timestamp', true);
+ }
+
+ /**
+ * Tests basic JavaScript linking using the <script /> tag
+ *
+ * @return void
+ */
+ public function testScriptLinking() {
+ $result = $this->html->script('script.js');
+ $expected = '<script type="text/javascript" src="/js/script.js"></script>';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->html->script('script');
+ $expected = '<script type="text/javascript" src="/js/script.js"></script>';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->html->script('scriptaculous.js?load=effects');
+ $expected = '<script type="text/javascript"';
+ $expected .= ' src="/js/scriptaculous.js?load=effects"></script>';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->html->script('jquery-1.1.2');
+ $expected = '<script type="text/javascript" src="/js/jquery-1.1.2.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script('jquery-1.1.2');
+ $expected = '<script type="text/javascript" src="/js/jquery-1.1.2.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script('/plugin/js/jquery-1.1.2');
+ $expected = '<script type="text/javascript" src="/plugin/js/jquery-1.1.2.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script('/some_other_path/myfile.1.2.2.min.js');
+ $expected = '<script type="text/javascript"';
+ $expected .= ' src="/some_other_path/myfile.1.2.2.min.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script('some_other_path/myfile.1.2.2.min.js');
+ $expected = '<script type="text/javascript"';
+ $expected .= ' src="/js/some_other_path/myfile.1.2.2.min.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script('some_other_path/myfile.1.2.2.min');
+ $expected = '<script type="text/javascript"';
+ $expected .= ' src="/js/some_other_path/myfile.1.2.2.min.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script('http://example.com/jquery.js');
+ $expected = '<script type="text/javascript" src="http://example.com/jquery.js"></script>';
+ $this->assertEqual($result, $expected);
+
+ $result = $this->html->script(array('prototype', 'scriptaculous'));
+ $this->assertPattern(
+ '/^\s*<script\s+type="text\/javascript"\s+src=".*js\/prototype\.js"[^<>]*><\/script>/',
+ $result
+ );
+ $this->assertPattern('/<\/script>\s*<script[^<>]+>/', $result);
+ $this->assertPattern(
+ '/<script\s+type="text\/javascript"\s+src=".*js\/scriptaculous\.js"[^<>]*>' .
+ '<\/script>\s*$/',
+ $result
+ );
+ }
+
+ /**
+ * Tests generating images with links wrapping them
+ *
+ * @return void
+ */
+ public function testImageLinking() {
+ $this->skipIf(true, "Not implemented");
+
+ $result = $this->html->image('test.gif', array('url' => '#'));
+ $expected = array(
+ 'a' => array('href' => '#'),
+ 'img' => array('src' => 'preg:/img\/test\.gif\?\d*/', 'alt' => ''),
+ '/a'
+ );
+ $this->assertTags($result, $expected);
+ }
+
+ /**
+ * Tests generating image tags
+ *
+ * @return void
+ */
+ function testImage() {
+
+ $result = $this->html->image('test.gif');
+ $this->assertTags($result, array('img' => array('src' => '/img/test.gif', 'alt' => '')));
+
+ $result = $this->html->image('http://google.com/logo.gif');
+ $this->assertTags($result, array('img' => array(
+ 'src' => 'http://google.com/logo.gif', 'alt' => ''
+ )));
+
+ $result = $this->html->image(array(
+ 'controller' => 'test', 'action' => 'view', 'id' => '1', 'type' => 'gif'
+ ));
+ $this->assertTags($result, array('img' => array('src' => '/test/view/1.gif', 'alt' => '')));
+
+ $result = $this->html->image('/test/view/1.gif');
+ $this->assertTags($result, array('img' => array('src' => '/test/view/1.gif', 'alt' => '')));
+ }
+
+ /**
+ * Tests inline style linking with <link /> tags
+ *
+ * @return void
+ */
+ public function testStyleLink() {
+ $result = $this->html->style('screen');
+ $expected = array('link' => array(
+ 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'preg:/.*css\/screen\.css/'
+ ));
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->style('screen.css');
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->style('screen.css?1234');
+ $expected['link']['href'] = 'preg:/.*css\/screen\.css\?1234/';
+ $this->assertTags($result, $expected);
+
+ $result = $this->html->style('http://whatever.com/screen.css?1234');
+ $expected['link']['href'] = 'preg:/http:\/\/.*\/screen\.css\?1234/';
+ $this->assertTags($result, $expected);
+
+// Configure::write('Asset.filter.css', 'css.php');
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/.*ccss\/lithium\.generic\.css/';
+// $this->assertTags($result, $expected);
+// Configure::write('Asset.filter.css', false);
+//
+// $result = explode("\n", trim($this->html->style(array('lithium.generic', 'vendor.generic'))));
+// $expected['link']['href'] = 'preg:/.*css\/lithium\.generic\.css/';
+// $this->assertTags($result[0], $expected);
+// $expected['link']['href'] = 'preg:/.*css\/vendor\.generic\.css/';
+// $this->assertTags($result[1], $expected);
+// $this->assertEqual(count($result), 2);
+//
+// Configure::write('Asset.timestamp', true);
+//
+// Configure::write('Asset.filter.css', 'css.php');
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/.*ccss\/lithium\.generic\.css\?[0-9]+/';
+// $this->assertTags($result, $expected);
+// Configure::write('Asset.filter.css', false);
+//
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/.*css\/lithium\.generic\.css\?[0-9]+/';
+// $this->assertTags($result, $expected);
+//
+// $debug = Configure::read('debug');
+// Configure::write('debug', 0);
+//
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/.*css\/lithium\.generic\.css/';
+// $this->assertTags($result, $expected);
+//
+// Configure::write('Asset.timestamp', 'force');
+//
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/.*css\/lithium\.generic\.css\?[0-9]+/';
+// $this->assertTags($result, $expected);
+//
+// $webroot = $this->html->webroot;
+// $this->html->webroot = '/testing/';
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/\/testing\/css\/lithium\.generic\.css\?/';
+// $this->assertTags($result, $expected);
+// $this->html->webroot = $webroot;
+//
+// $webroot = $this->html->webroot;
+// $this->html->webroot = '/testing/longer/';
+// $result = $this->html->style('lithium.generic');
+// $expected['link']['href'] = 'preg:/\/testing\/longer\/css\/lithium\.generic\.css\?/';
+// $this->assertTags($result, $expected);
+// $this->html->webroot = $webroot;
+//
+// Configure::write('debug', $debug);
+ }
+
+ /**
+ * Tests generating multiple <link /> or <style /> tags in a single call with an array
+ *
+ * @return void
+ */
+ public function testStyleMulti() {
+ $result = $this->html->style(array('base', 'layout'));
+ $expected = array(
+ 'link' => array(
+ 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'preg:/.*css\/base\.css/'
+ ),
+ array(
+ 'link' => array(
+ 'rel' => 'stylesheet', 'type' => 'text/css',
+ 'href' => 'preg:/.*css\/layout\.css/'
+ )
+ )
+ );
+ $this->assertTags($result, $expected);
+ }
+
+ /**
+ * Tests arbitrary tag generation.
+ *
+ * @return void
+ */
+ public function testTag() {
+ $result = $this->html->tag('div');
+ $this->assertTags($result, '<div');
+
+ $result = $this->html->tag('div', 'text');
+ $this->assertTags($result, '<div', 'text', '/div');
+
+ $result = $this->html->tag('div', '<text>', array('class' => 'class-name'), true);
+ $this->assertTags($result, array(
+ 'div' => array('class' => 'class-name'), '<text>', '/div'
+ ));
+
+ $result = $this->html->tag('div', '<text>', 'class-name', true);
+ $this->assertTags($result, array(
+ 'div' => array('class' => 'class-name'), '<text>', '/div'
+ ));
+ }
+
+ /**
+ * Tests generation of block-level element (<div />).
+ *
+ * @return void
+ */
+ public function testBlock() {
+ $result = $this->html->block('class-name');
+ $this->assertTags($result, array('div' => array('class' => 'class-name')));
+
+ $result = $this->html->block('class-name', 'text');
+ $this->assertTags($result, array('div' => array('class' => 'class-name'), 'text', '/div'));
+
+ $result = $this->html->block('class-name', '<text>', array(), true);
+ $this->assertTags($result, array(
+ 'div' => array('class' => 'class-name'), '<text>', '/div'
+ ));
+ }
+
+ /**
+ * Tests paragraph generation.
+ *
+ * @return void
+ */
+ function testPara() {
+ $result = $this->html->para('class-name', '');
+ $this->assertTags($result, array('p' => array('class' => 'class-name')));
+
+ $result = $this->html->para('class-name', 'text');
+ $this->assertTags($result, array('p' => array('class' => 'class-name'), 'text', '/p'));
+
+ $result = $this->html->para('class-name', '<text>', array(), true);
+ $this->assertTags($result, array(
+ 'p' => array('class' => 'class-name'), '<text>', '/p'
+ ));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/view/RendererTest.php b/libraries/lithium/tests/cases/template/view/RendererTest.php
new file mode 100644
index 0000000..6c0ff43
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/view/RendererTest.php
@@ -0,0 +1,155 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\template\view;
+
+use \lithium\action\Request;
+use \lithium\template\Helper;
+use \lithium\template\helpers\Html;
+use \lithium\template\view\adapters\Simple;
+
+class RendererTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->subject = new Simple();
+ }
+
+ public function testInitialization() {
+ $subject = new Simple();
+
+ $expected = array('url', 'path', 'options', 'content', 'title', 'scripts');
+ $result = array_keys($subject->handlers());
+ $this->assertEqual($expected, $result);
+
+ $expected = array('content', 'title', 'scripts', 'styles');
+ $result = array_keys($subject->context());
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testContextQuerying() {
+ $expected = array(
+ 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array()
+ );
+ $this->assertEqual($expected, $this->subject->context());
+ $this->assertEqual('', $this->subject->context('title'));
+ $this->assertEqual(array(), $this->subject->context('scripts'));
+ $this->assertEqual(array(), $this->subject->scripts);
+ $this->assertNull($this->subject->foo());
+
+ $this->subject = new Simple(array('context' => array(
+ 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array(), 'foo' => '!'
+ )));
+ $result = $this->subject->foo();
+ $expected = '!';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHandlerInsertion() {
+ $this->subject = new Simple(array('context' => array(
+ 'content' => '', 'title' => '', 'scripts' => array(), 'styles' => array(), 'foo' => '!'
+ )));
+
+ $foo = function($value) { return "Foo: {$value}"; };
+
+ $expected = array('url', 'path', 'options', 'content', 'title', 'scripts', 'foo');
+ $result = array_keys($this->subject->handlers(compact('foo')));
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->applyHandler(null, null, 'foo', 'test value');
+ $this->assertEqual('Foo: test value', $result);
+ $this->assertEqual('Foo: !', $this->subject->foo());
+
+ $this->assertEqual($foo, $this->subject->handlers('foo'));
+ $this->assertNull($this->subject->handlers('bar'));
+
+ $bar = function($value) { return "Bar: {$value}"; };
+
+ $this->subject->handlers(compact('bar'));
+ $result = $this->subject->applyHandler(null, null, 'bar', 'test value');
+ $expected = 'Bar: test value';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->bar('test value');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHandlerQuerying() {
+ $result = $this->subject->nonExistent('test value');
+ $expected = 'test value';
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->applyHandler(null, null, 'nonExistent', 'test value');
+ $this->assertEqual($expected, $result);
+
+ $html = new Html();
+ $script = '<script>alert("XSS!");</script>';
+ $escaped = '<script>alert("XSS!");</script>';
+
+ $result = $this->subject->applyHandler($html, null, 'title', $script);
+ $expected = $escaped;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->applyHandler($html, null, 'foo', $script);
+ $expected = $script;
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->applyHandler($html, null, 'bar', $script);
+ $this->assertEqual($expected, $result);
+
+ $this->subject->handlers(array('foo' => array($html, 'escape'), 'bar' => 42));
+
+ $result = $this->subject->applyHandler($html, null, 'bar', $script);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->applyHandler($html, null, 'foo', $script);
+ $expected = $escaped;
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testHelperLoading() {
+ $helper = $this->subject->helper('html');
+ $this->assertTrue($helper instanceof Helper);
+
+ $this->expectException('/invalidFoo/');
+ $this->assertNull($this->subject->helper('invalidFoo'));
+ }
+
+ public function testHelperQuerying() {
+ $helper = $this->subject->html;
+ $this->assertTrue($helper instanceof Helper);
+ }
+
+ public function testTemplateStrings() {
+ $result = $this->subject->strings();
+ $this->assertTrue(is_array($result));
+ $this->assertFalse($result);
+
+ $expected = array('data' => 'The data goes here: {:data}');
+ $result = $this->subject->strings($expected);
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->strings();
+ $this->assertEqual($expected, $result);
+
+ $result = $this->subject->strings('data');
+ $expected = $expected['data'];
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testGetters() {
+ $this->assertNull($this->subject->request());
+ $this->subject = new Simple(array('request' => new Request()));
+ $this->assertTrue($this->subject->request() instanceof Request);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/template/view/adapters/SimpleTest.php b/libraries/lithium/tests/cases/template/view/adapters/SimpleTest.php
new file mode 100644
index 0000000..76ba5f2
--- /dev/null
+++ b/libraries/lithium/tests/cases/template/view/adapters/SimpleTest.php
@@ -0,0 +1,30 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\template\view\adapters;
+
+use \lithium\template\view\adapters\Simple;
+
+class SimpleTest extends \lithium\test\Unit {
+
+ protected $_simple = null;
+
+ public function setUp() {
+ $this->_simple = new Simple();
+ }
+
+ public function testFoo() {
+ $this->_simple = new Simple();
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/GroupTest.php b/libraries/lithium/tests/cases/test/GroupTest.php
new file mode 100644
index 0000000..ac88f75
--- /dev/null
+++ b/libraries/lithium/tests/cases/test/GroupTest.php
@@ -0,0 +1,116 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class GroupTest extends \lithium\test\Unit {
+
+ public function testAdd() {
+ $group = new Group();
+ $result = $group->add('\g11n');
+ $expected = array(
+ 'lithium\tests\cases\g11n\CatalogTest',
+ 'lithium\tests\cases\g11n\LocaleTest',
+ 'lithium\tests\cases\g11n\MessageTest',
+ 'lithium\tests\cases\g11n\catalog\adapters\CldrTest',
+ 'lithium\tests\cases\g11n\catalog\adapters\CodeTest',
+ 'lithium\tests\cases\g11n\catalog\adapters\GettextTest',
+ );
+ $this->assertEqual($expected, $result);
+
+ $result = $group->add('data\ModelTest');
+ $expected = array(
+ 'lithium\tests\cases\g11n\CatalogTest',
+ 'lithium\tests\cases\g11n\LocaleTest',
+ 'lithium\tests\cases\g11n\MessageTest',
+ 'lithium\tests\cases\g11n\catalog\adapters\CldrTest',
+ 'lithium\tests\cases\g11n\catalog\adapters\CodeTest',
+ 'lithium\tests\cases\g11n\catalog\adapters\GettextTest',
+ 'lithium\tests\cases\data\ModelTest'
+ );
+ $this->assertEqual($expected, $result);
+
+ $group = new Group();
+ $result = $group->add();
+ $this->assertEqual($group->tests(), new Collection());
+
+ $expected = new Collection(array('items' => array(
+ new \lithium\tests\cases\data\ModelTest(),
+ new \lithium\tests\cases\core\ObjectTest()
+ )));
+
+ $group = new Group(array('items' => array(
+ '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);
+ }
+
+ public function testTests() {
+ $group = new Group();
+ $result = $group->add('g11n\CatalogTest');
+ $expected = array(
+ 'lithium\tests\cases\g11n\CatalogTest',
+ );
+ $this->assertEqual($expected, $result);
+
+ $results = $group->tests();
+ $this->assertTrue(is_a($results, '\lithium\util\Collection'));
+
+ $results = $group->tests();
+ $this->assertTrue(is_a($results->current(), 'lithium\tests\cases\g11n\CatalogTest'));
+ }
+
+ public function testTestsRun() {
+ $group = new Group();
+ $result = $group->add('test\MockTestInGroupTest');
+ $expected = array(
+ 'lithium\tests\cases\test\MockTestInGroupTest',
+ );
+ $this->assertEqual($expected, $result);
+
+ $results = $group->tests();
+ $this->assertTrue(is_a($results, '\lithium\util\Collection'));
+
+ $results = $group->tests();
+ $this->assertTrue(is_a($results->current(), 'lithium\tests\cases\test\MockTestInGroupTest'));
+
+ $results = $group->tests()->run();
+ $this->assertEqual($results[0][0]['result'], 'pass');
+ $this->assertEqual($results[0][0]['method'], 'testNothing');
+ $this->assertEqual($results[0][0]['file'], __FILE__);
+ $this->assertEqual($results[0][0]['class'], 'lithium\tests\cases\test\MockTestInGroupTest');
+ }
+
+ public function testQueryAllTests() {
+ $result = Group::all(array('library' => 'lithium'));
+ $this->assertEqual(54, count($result));
+ }
+}
+
+class MockTestInGroupTest extends \lithium\test\Unit {
+
+ public function testNothing() {
+ $this->assertTrue(true);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/ReporterTest.php b/libraries/lithium/tests/cases/test/ReporterTest.php
new file mode 100644
index 0000000..1feb93a
--- /dev/null
+++ b/libraries/lithium/tests/cases/test/ReporterTest.php
@@ -0,0 +1,21 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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 testRender() {
+
+ }
+}
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/test/UnitTest.php b/libraries/lithium/tests/cases/test/UnitTest.php
new file mode 100644
index 0000000..d6861d2
--- /dev/null
+++ b/libraries/lithium/tests/cases/test/UnitTest.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\Test;
+
+class UnitTest extends \lithium\test\Unit {
+
+ public function compare($type, $expected, $result = null) {
+ return $this->_compare($type, $expected, $result);
+ }
+
+ /**
+ * @todo Figure out a way to expect failures
+ * @return void
+ */
+ public function testBaseAssertions() {
+ $this->assert(true);
+ //$this->assert(false);
+ $this->assertTrue(true);
+ $this->assertFalse(false);
+ }
+
+ public function testCompare() {
+ $expected = array('trace' => null, 'expected' => 'array', 'result' => 'string');
+ $result = $this->compare(array(), 'string');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testAssertEqualNumeric() {
+ $expected = array(1, 2, 3);
+ $result = array(1, 2, 3);
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * undocumented function
+ *
+ * @return void
+ * @todo See @todo above.
+ */
+ public function testAssertEqualNumericFail() {
+ $result = array(1, 2);
+ $expected = array(1, 2, 3);
+ //$this->assertEqual($expected, $result);
+ }
+
+ public function testAssertEqualAssociativeArray() {
+ $expected = array(
+ 'expected' => 'array',
+ 'result' => 'string'
+ );
+ $result = array(
+ 'expected' => 'array',
+ 'result' => 'string'
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * undocumented function
+ *
+ * @return void
+ * @todo See @todo above.
+ */
+ public function testAssertEqualThreeDFail() {
+ $result = array(
+ array(array(1, 2), array(1)),
+ array(array(1, 2), array(1))
+ );
+ $expected = array(
+ array(array(1, 2), array(1, 2)),
+ array(array(1, 2), array(1, 2))
+ );
+ //$this->assertEqual($expected, $result);
+ }
+
+ public function testAssertIdentical() {
+
+ }
+
+ public function testTestMethods() {
+ $expected = array(
+ 'testBaseAssertions', 'testCompare', 'testAssertEqualNumeric',
+ 'testAssertEqualNumericFail', 'testAssertEqualAssociativeArray',
+ 'testAssertEqualThreeDFail', 'testAssertIdentical', 'testTestMethods'
+ );
+ $this->assertEqual($expected, $this->methods());
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/CollectionTest.php b/libraries/lithium/tests/cases/util/CollectionTest.php
new file mode 100644
index 0000000..9421f1f
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/CollectionTest.php
@@ -0,0 +1,246 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class DispatchTest {
+
+ public $marker = false;
+
+ public $data = 'foo';
+
+ public function mark() {
+ $this->marker = true;
+ return true;
+ }
+
+ function mapArray() {
+ return array('foo');
+ }
+}
+
+class CoreDispatchTest extends \lithium\core\Object {
+
+ public $data = array(1 => 2);
+
+ public function invokeMethod($method, $params = array()) {
+ return $method;
+ }
+
+ public function to($format, $options = array()) {
+ switch ($format) {
+ case 'array':
+ return $this->data + array(2 => 3);
+ }
+ }
+}
+
+class CollectionTest extends \lithium\test\Unit {
+
+ public function testArrayLike() {
+ $collection = new Collection();
+ $collection[] = 'foo';
+ $this->assertEqual($collection[0], 'foo');
+ $this->assertEqual(count($collection), 1);
+
+ $collection = new Collection(array('items' => array('foo')));
+ $this->assertEqual($collection[0], 'foo');
+ $this->assertEqual(count($collection), 1);
+ }
+
+ public function testObjectMethodDispatch() {
+ $collection = new Collection();
+
+ for ($i = 0; $i < 10; $i++) {
+ $collection[] = new DispatchTest();
+ }
+ $result = $collection->mark();
+ $this->assertEqual($result, array_fill(0, 10, true));
+
+ $result = $collection->mapArray();
+ $this->assertEqual($result, array_fill(0, 10, array('foo')));
+
+ $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 CoreDispatchTest())));
+ $result = $collection->testFoo();
+ $this->assertEqual($result, array_fill(0, 10, 'testFoo'));
+
+ $result = $collection->invoke('testFoo', array(), array('collect' => true));
+ $this->assertTrue($result instanceof Collection);
+ $this->assertEqual($result->to('array'), array_fill(0, 10, 'testFoo'));
+ }
+
+ public function testObjectCasting() {
+ $collection = new Collection(array('items' => array_fill(0, 10, new CoreDispatchTest())));
+ $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 DispatchTest())));
+ $result = $collection->to('array');
+ $expected = array_fill(0, 10, array('marker' => false, 'data' => 'foo'));
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests that the `find()` method properly filters items out of the resulting collection.
+ *
+ * @return void
+ */
+ public function testCollectionFindFilter() {
+ $collection = new Collection(array('items' => array_merge(
+ array_fill(0, 10, 1),
+ array_fill(0, 10, 2)
+ )));
+ $this->assertEqual(20, count($collection->to('array')));
+
+ $filter = function($item) { return $item == 1; };
+ $result = $collection->find($filter);
+ $this->assertTrue($result instanceof Collection);
+ $this->assertEqual(array_fill(0, 10, 1), $result->to('array'));
+
+ $result = $collection->find($filter, array('collect' => false));
+ $this->assertEqual(array_fill(0, 10, 1), $result);
+ }
+
+ /**
+ * Tests that the `first()` method properly returns the first non-empty value.
+ *
+ * @return void
+ */
+ public function testCollectionFirstFilter() {
+ $collection = new Collection(array('items' => array(0, 1, 2)));
+ $result = $collection->first(function($value) { return $value; });
+ $this->assertEqual(1, $result);
+
+ $collection = new Collection(array('items' => array('Hello', '', 'Goodbye')));
+ $result = $collection->first(function($value) { return $value; });
+ $this->assertEqual('Hello', $result);
+
+ $collection = new Collection(array('items' => array('', 'Hello', 'Goodbye')));
+ $result = $collection->first(function($value) { return $value; });
+ $this->assertEqual('Hello', $result);
+ }
+
+ /**
+ * Tests that the `each()` filter applies the callback to each item in the current collection,
+ * returning an instance of itself.
+ *
+ * @return void
+ */
+ public function testCollectionEachFilter() {
+ $collection = new Collection(array('items' => array(1, 2, 3, 4, 5)));
+ $filter = function($item) { return ++$item; };
+ $result = $collection->each($filter);
+
+ $this->assertIdentical($collection, $result);
+ $this->assertEqual(array(2, 3, 4, 5, 6), $collection->to('array'));
+ }
+
+ public function testCollectionMapFilter() {
+ $collection = new Collection(array('items' => array(1, 2, 3, 4, 5)));
+ $filter = function($item) { return ++$item; };
+ $result = $collection->map($filter);
+
+ $this->assertNotEqual($collection, $result);
+ $this->assertEqual(array(1, 2, 3, 4, 5), $collection->to('array'));
+ $this->assertEqual(array(2, 3, 4, 5, 6), $result->to('array'));
+
+ $result = $collection->map($filter, array('collect' => false));
+ $this->assertEqual(array(2, 3, 4, 5, 6), $result);
+ }
+
+ /**
+ * Tests the `ArrayAccess` interface implementation for manipulating values by direct offsets.
+ *
+ * @return void
+ */
+ public function testArrayAccessOffsetMethods() {
+ $collection = new Collection(array('items' => array('foo', 'bar', 'baz' => 'dib')));
+ $this->assertTrue($collection->offsetExists(0));
+ $this->assertTrue($collection->offsetExists(1));
+ $this->assertTrue($collection->offsetExists('0'));
+ $this->assertTrue($collection->offsetExists('baz'));
+
+ $this->assertFalse($collection->offsetExists('2'));
+ $this->assertFalse($collection->offsetExists('bar'));
+ $this->assertFalse($collection->offsetExists(2));
+
+ $this->assertEqual('foo', $collection->offsetSet('bar', 'foo'));
+ $this->assertTrue($collection->offsetExists('bar'));
+
+ $this->assertNull($collection->offsetUnset('bar'));
+ $this->assertFalse($collection->offsetExists('bar'));
+ }
+
+ /**
+ * Tests the `ArrayAccess` interface implementation for traversing values.
+ *
+ * @return void
+ */
+ public function testArrayAccessTraversalMethods() {
+ $collection = new Collection(array('items' => array('foo', 'bar', 'baz' => 'dib')));
+ $this->assertEqual('foo', $collection->current());
+ $this->assertEqual('bar', $collection->next());
+ $this->assertEqual('foo', $collection->prev());
+ $this->assertEqual('bar', $collection->next());
+ $this->assertEqual('dib', $collection->next());
+ $this->assertEqual('baz', $collection->key());
+ $this->assertTrue($collection->valid());
+ $this->assertFalse($collection->next());
+ $this->assertFalse($collection->valid());
+ $this->assertEqual('foo', $collection->rewind());
+ $this->assertTrue($collection->valid());
+ $this->assertEqual('dib', $collection->prev());
+ $this->assertTrue($collection->valid());
+ $this->assertEqual('bar', $collection->prev());
+ $this->assertTrue($collection->valid());
+ $this->assertEqual('dib', $collection->end());
+ $this->assertTrue($collection->valid());
+ }
+
+ /**
+ * Tests objects and scalar values being appended to the collection.
+ *
+ * @return void
+ */
+ public function testValueAppend() {
+ $collection = new Collection();
+ $this->assertFalse($collection->valid());
+ $this->assertEqual(0, count($collection));
+
+ $collection->append(1);
+ $this->assertEqual(1, count($collection));
+ $collection->append(new stdClass());
+ $this->assertEqual(2, count($collection));
+
+ $this->assertEqual(1, $collection->current());
+ $this->assertEqual(new stdClass(), $collection->next());
+ }
+
+ /**
+ * Tests getting the index of the internal array.
+ *
+ * @return void
+ */
+ public function testInternalKeys() {
+ $collection = new Collection(array('items' => array('foo', 'bar', 'baz' => 'dib')));
+ $this->assertEqual(array(0, 1, 'baz'), $collection->keys());
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/InflectorTest.php b/libraries/lithium/tests/cases/util/InflectorTest.php
new file mode 100644
index 0000000..9c2506b
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/InflectorTest.php
@@ -0,0 +1,321 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class InflectorTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Inflector::__init();
+ }
+
+ /**
+ * Tests singularization inflection rules
+ *
+ * @return void
+ */
+ public function testInflectingSingulars() {
+ $this->assertEqual(Inflector::singularize('categorias'), 'categoria');
+ $this->assertEqual(Inflector::singularize('menus'), 'menu');
+ $this->assertEqual(Inflector::singularize('news'), 'news');
+ $this->assertEqual(Inflector::singularize('food_menus'), 'food_menu');
+ $this->assertEqual(Inflector::singularize('Menus'), 'Menu');
+ $this->assertEqual(Inflector::singularize('FoodMenus'), 'FoodMenu');
+ $this->assertEqual(Inflector::singularize('houses'), 'house');
+ $this->assertEqual(Inflector::singularize('powerhouses'), 'powerhouse');
+ $this->assertEqual(Inflector::singularize('quizzes'), 'quiz');
+ $this->assertEqual(Inflector::singularize('Buses'), 'Bus');
+ $this->assertEqual(Inflector::singularize('buses'), 'bus');
+ $this->assertEqual(Inflector::singularize('matrix_rows'), 'matrix_row');
+ $this->assertEqual(Inflector::singularize('matrices'), 'matrix');
+ $this->assertEqual(Inflector::singularize('vertices'), 'vertex');
+ $this->assertEqual(Inflector::singularize('indices'), 'index');
+ $this->assertEqual(Inflector::singularize('Aliases'), 'Alias');
+ $this->assertEqual(Inflector::singularize('Alias'), 'Alias');
+ $this->assertEqual(Inflector::singularize('Media'), 'Media');
+ $this->assertEqual(Inflector::singularize('alumni'), 'alumnus');
+ $this->assertEqual(Inflector::singularize('bacilli'), 'bacillus');
+ $this->assertEqual(Inflector::singularize('cacti'), 'cactus');
+ $this->assertEqual(Inflector::singularize('foci'), 'focus');
+ $this->assertEqual(Inflector::singularize('fungi'), 'fungus');
+ $this->assertEqual(Inflector::singularize('nuclei'), 'nucleus');
+ $this->assertEqual(Inflector::singularize('octopuses'), 'octopus');
+ $this->assertEqual(Inflector::singularize('radii'), 'radius');
+ $this->assertEqual(Inflector::singularize('stimuli'), 'stimulus');
+ $this->assertEqual(Inflector::singularize('syllabi'), 'syllabus');
+ $this->assertEqual(Inflector::singularize('termini'), 'terminus');
+ $this->assertEqual(Inflector::singularize('viri'), 'virus');
+ $this->assertEqual(Inflector::singularize('people'), 'person');
+ $this->assertEqual(Inflector::singularize('gloves'), 'glove');
+ $this->assertEqual(Inflector::singularize('doves'), 'dove');
+ $this->assertEqual(Inflector::singularize('lives'), 'life');
+ $this->assertEqual(Inflector::singularize('knives'), 'knife');
+ $this->assertEqual(Inflector::singularize('wolves'), 'wolf');
+ $this->assertEqual(Inflector::singularize('shelves'), 'shelf');
+ $this->assertEqual(Inflector::singularize(''), '');
+ }
+
+ /**
+ * Tests pluralization inflection rules
+ *
+ * @return void
+ */
+ public function testInflectingPlurals() {
+ $this->assertEqual(Inflector::pluralize('categoria'), 'categorias');
+ $this->assertEqual(Inflector::pluralize('house'), 'houses');
+ $this->assertEqual(Inflector::pluralize('powerhouse'), 'powerhouses');
+ $this->assertEqual(Inflector::pluralize('Bus'), 'Buses');
+ $this->assertEqual(Inflector::pluralize('bus'), 'buses');
+ $this->assertEqual(Inflector::pluralize('menu'), 'menus');
+ $this->assertEqual(Inflector::pluralize('news'), 'news');
+ $this->assertEqual(Inflector::pluralize('food_menu'), 'food_menus');
+ $this->assertEqual(Inflector::pluralize('Menu'), 'Menus');
+ $this->assertEqual(Inflector::pluralize('FoodMenu'), 'FoodMenus');
+ $this->assertEqual(Inflector::pluralize('quiz'), 'quizzes');
+ $this->assertEqual(Inflector::pluralize('matrix_row'), 'matrix_rows');
+ $this->assertEqual(Inflector::pluralize('matrix'), 'matrices');
+ $this->assertEqual(Inflector::pluralize('vertex'), 'vertices');
+ $this->assertEqual(Inflector::pluralize('index'), 'indices');
+ $this->assertEqual(Inflector::pluralize('Alias'), 'Aliases');
+ $this->assertEqual(Inflector::pluralize('Aliases'), 'Aliases');
+ $this->assertEqual(Inflector::pluralize('Media'), 'Media');
+ $this->assertEqual(Inflector::pluralize('alumnus'), 'alumni');
+ $this->assertEqual(Inflector::pluralize('bacillus'), 'bacilli');
+ $this->assertEqual(Inflector::pluralize('cactus'), 'cacti');
+ $this->assertEqual(Inflector::pluralize('focus'), 'foci');
+ $this->assertEqual(Inflector::pluralize('fungus'), 'fungi');
+ $this->assertEqual(Inflector::pluralize('nucleus'), 'nuclei');
+ $this->assertEqual(Inflector::pluralize('octopus'), 'octopuses');
+ $this->assertEqual(Inflector::pluralize('radius'), 'radii');
+ $this->assertEqual(Inflector::pluralize('stimulus'), 'stimuli');
+ $this->assertEqual(Inflector::pluralize('syllabus'), 'syllabi');
+ $this->assertEqual(Inflector::pluralize('terminus'), 'termini');
+ $this->assertEqual(Inflector::pluralize('virus'), 'viri');
+ $this->assertEqual(Inflector::pluralize('person'), 'people');
+ $this->assertEqual(Inflector::pluralize('people'), 'people');
+ $this->assertEqual(Inflector::pluralize('glove'), 'gloves');
+ $this->assertEqual(Inflector::pluralize(''), '');
+
+ $result = Inflector::pluralize('errata');
+ $this->assertNull(Inflector::rules('plural', array('/rata/' => '\1ratum')));
+ $this->assertEqual(Inflector::pluralize('errata'), $result);
+
+ Inflector::clear();
+ $this->assertNotEqual(Inflector::pluralize('errata'), $result);
+ }
+
+ /**
+ * testInflectorSlug method
+ *
+ * @return void
+ */
+ public function testInflectorSlug() {
+ $result = Inflector::slug('Foo Bar: Not just for breakfast any-more');
+ $expected = 'Foo_Bar_Not_just_for_breakfast_any_more';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('this/is/a/path');
+ $expected = 'this_is_a_path';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('Foo Bar: Not just for breakfast any-more', "-");
+ $expected = 'Foo-Bar-Not-just-for-breakfast-any-more';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('Foo Bar: Not just for breakfast any-more', "+");
+ $expected = 'Foo+Bar+Not+just+for+breakfast+any+more';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('Äpfel Über Öl grün ärgert groß öko', '-');
+ $expected = 'Aepfel-Ueber-Oel-gruen-aergert-gross-oeko';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('The truth - and- more- news', '-');
+ $expected = 'The-truth-and-more-news';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('The truth: and more news', '-');
+ $expected = 'The-truth-and-more-news';
+ $this->assertEqual($expected, $result);
+
+ $message = 'La langue française est un attribut de souveraineté en France';
+ $result = Inflector::slug($message, '-');
+ $expected = 'La-langue-francaise-est-un-attribut-de-souverainete-en-France';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('!@$#exciting stuff! - what !@-# was that?', '-');
+ $expected = 'exciting-stuff-what-was-that';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('20% of profits went to me!', '-');
+ $expected = '20-of-profits-went-to-me';
+ $this->assertEqual($expected, $result);
+
+ $result = Inflector::slug('#this melts your face1#2#3', '-');
+ $expected = 'this-melts-your-face1-2-3';
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testAddingInvalidRules() {
+ $before = array(
+ Inflector::rules('singular'),
+ Inflector::rules('plural'),
+ Inflector::rules('transliterations')
+ );
+ $this->assertNull(Inflector::rules('foo'));
+ $this->assertIdentical($before, array(
+ Inflector::rules('singular'),
+ Inflector::rules('plural'),
+ Inflector::rules('transliterations')
+ ));
+ }
+
+ public function testAddingSingularizationRules() {
+ $before = Inflector::rules('singular');
+ $result = Inflector::singularize('errata');
+ $this->assertNull(Inflector::rules('singular', array('/rata/' => '\1ratus')));
+ $this->assertEqual(Inflector::singularize('errata'), $result);
+
+ Inflector::clear();
+ $this->assertNotEqual(Inflector::singularize('errata'), $result);
+
+ $after = Inflector::rules('singular');
+ $expected = array(
+ 'rules', 'irregular', 'uninflected', 'regexUninflected', 'regexIrregular'
+ );
+ $this->assertEqual(array_keys($before), $expected);
+ $this->assertEqual(array_keys($after), $expected);
+
+ $result = array_diff($after['rules'], $before['rules']);
+ $this->assertEqual($result, array('/rata/' => '\1ratus'));
+
+ foreach (array('irregular', 'uninflected', 'regexUninflected', 'regexIrregular') as $key) {
+ $this->assertIdentical($before[$key], $after[$key]);
+ }
+
+ $this->assertNull(Inflector::rules('singular', array('rules' => array(
+ '/rata/' => '\1ratus'
+ ))));
+ $this->assertIdentical(Inflector::rules('singular'), $after);
+ }
+
+ /**
+ * Tests that rules for uninflected singular words are kept in sync with the plural, and vice
+ * versa.
+ *
+ * @return void
+ */
+ public function testIrregularSynchronicity() {
+ $expectedPlural = Inflector::rules('plural');
+ $this->assertFalse(isset($expectedPlural['irregular']['bar']));
+
+ $expectedSingular = Inflector::rules('singular');
+ $this->assertFalse(isset($expectedSingular['irregular']['foo']));
+
+ Inflector::rules('singular', array('irregular' => array('foo' => 'bar')));
+
+ $resultSingular = Inflector::rules('singular');
+ $this->assertEqual($resultSingular['irregular']['foo'], 'bar');
+ unset($resultSingular['irregular']['foo']);
+
+ $this->assertEqual($resultSingular, $expectedSingular);
+
+ $resultPlural = Inflector::rules('plural');
+ $this->assertEqual($resultPlural['irregular']['bar'], 'foo');
+ unset($resultPlural['irregular']['bar']);
+
+ $this->assertEqual($resultPlural, $expectedPlural);
+ }
+
+ /**
+ * testVariableNaming method
+ *
+ * @return void
+ */
+ public function testVariableNaming() {
+ $this->assertEqual(Inflector::variable('test_field'), 'testField');
+ $this->assertEqual(Inflector::variable('test_fieLd'), 'testFieLd');
+ $this->assertEqual(Inflector::variable('test field'), 'testField');
+ $this->assertEqual(Inflector::variable('Test_field'), 'testField');
+ }
+
+ /**
+ * testClassNaming method
+ *
+ * @return void
+ */
+ public function testClassNaming() {
+ $this->assertEqual(Inflector::classify('artists_genres'), 'ArtistsGenre');
+ $this->assertEqual(Inflector::classify('file_systems'), 'FileSystem');
+ $this->assertEqual(Inflector::classify('news'), 'News');
+ }
+
+ /**
+ * testTableNaming method
+ *
+ * @return void
+ */
+ public function testTableNaming() {
+ $this->assertEqual(Inflector::tableize('ArtistsGenre'), 'artists_genres');
+ $this->assertEqual(Inflector::tableize('FileSystem'), 'file_systems');
+ $this->assertEqual(Inflector::tableize('News'), 'news');
+ }
+
+ /**
+ * testHumanization method
+ *
+ * @return void
+ */
+ public function testHumanization() {
+ $this->assertEqual(Inflector::humanize('posts'), 'Posts');
+ $this->assertEqual(Inflector::humanize('posts_tags'), 'Posts Tags');
+ $this->assertEqual(Inflector::humanize('file_systems'), 'File Systems');
+ $this->assertEqual(Inflector::humanize('the-post-title', '-'), 'The Post Title');
+ }
+
+ /**
+ * Tests adding transliterated characters to the map used in `Inflector::slug()`.
+ *
+ * @return void
+ */
+ public function testAddTransliterations() {
+ Inflector::__init();
+ $this->assertEqual(Inflector::slug('Montréal'), 'Montreal');
+ $this->assertNotEqual(Inflector::slug('Écaussines'), 'Ecaussines');
+
+ Inflector::rules('transliterations', array('/É|Ê/' => 'E'));
+ $this->assertEqual(Inflector::slug('Écaussines-d\'Enghien', '-'), 'Ecaussines-d-Enghien');
+
+ $this->assertNotEqual(Inflector::slug('JØRGEN'), 'JORGEN');
+ Inflector::rules('transliterations', array('/Ø/' => 'O'));
+ $this->assertEqual(Inflector::slug('JØRGEN'), 'JORGEN');
+
+ $this->assertNotEqual(Inflector::slug('ÎÍ'), 'II');
+ Inflector::rules('transliterations', array('/Î|Í/' => 'I'));
+ $this->assertEqual(Inflector::slug('ÎÍ'), 'II');
+
+ $this->assertEqual(Inflector::slug('ABc'), 'ABc');
+ Inflector::rules('transliterations', array('AB' => 'a'));
+ Inflector::clear();
+ $this->assertEqual(Inflector::slug('ABc'), 'aac');
+ }
+
+ public function testAddingUninflectedWords() {
+ $this->assertEqual(Inflector::pluralize('bord'), 'bords');
+ Inflector::rules('uninflected', 'bord');
+ $this->assertEqual(Inflector::pluralize('bord'), 'bord');
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/SetTest.php b/libraries/lithium/tests/cases/util/SetTest.php
new file mode 100644
index 0000000..6e73f69
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/SetTest.php
@@ -0,0 +1,2070 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class SetTest extends \lithium\test\Unit {
+
+ /**
+ * testDepthWithEmptyData method
+ *
+ * @return void
+ */
+ public function testDepthWithEmptyData() {
+ $data = array();
+ $result = Set::depth($data);
+ $this->assertEqual($result, 0);
+ }
+
+ /**
+ * testDepthOneLevelWithDefaults method
+ *
+ * @return void
+ */
+ public function testDepthOneLevelWithDefaults() {
+ $data = array();
+ $result = Set::depth($data);
+ $this->assertEqual($result, 0);
+
+ $data = array('one', '2', 'three');
+ $result = Set::depth($data);
+ $this->assertEqual($result, 1);
+
+ $data = array('1' => '1.1', '2', '3');
+ $result = Set::depth($data);
+ $this->assertEqual($result, 1);
+
+ $data = array('1' => '1.1', '2', '3' => array('3.1' => '3.1.1'));
+ $result = Set::depth($data, false, 0);
+ $this->assertEqual($result, 1);
+ }
+
+ /**
+ * testDepthTwoLevelsWithDefaults method
+ *
+ * @return void
+ */
+ 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);
+
+ $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);
+ $this->assertEqual($result, 2);
+
+ $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, false, 0);
+ $this->assertEqual($result, 2);
+ }
+ /**
+ * testDepthTwoLevelsWithDefaults method
+ *
+ * @return void
+ */
+ 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);
+ }
+
+ /**
+ * testDepthThreeLevelsWithAll method
+ *
+ * @return void
+ */
+ public function testDepthThreeLevelsWithAll() {
+ $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);
+
+ $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);
+
+ $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);
+
+ $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);
+ }
+
+ /**
+ * testDepthFourLevelsWithAll method
+ *
+ * @return void
+ */
+ public function testDepthFourLevelsWithAll() {
+ $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);
+ }
+
+ /**
+ * testDepthFiveLevelsWithAll method
+ *
+ * @return void
+ */
+ public function testDepthFiveLevelsWithAll() {
+
+ $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);
+
+ $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);
+ }
+
+ /**
+ * testFlattenOneLevel
+ *
+ * @return void
+ */
+ public function testFlattenOneLevel() {
+ $data = array('Larry', 'Curly', 'Moe');
+ $result = Set::flatten($data);
+ $this->assertEqual($result, $data);
+
+ $data[9] = 'Shemp';
+ $result = Set::flatten($data);
+ $this->assertEqual($result, $data);
+ }
+
+ /**
+ * testFlattenTwoLevels
+ *
+ * @return void
+ */
+ public function testFlattenTwoLevels() {
+ $data = array(
+ array(
+ 'Post' => array('id' => '1', 'author_id' => '1', 'title' => 'First Post'),
+ 'Author' => array('id' => '1', 'user' => 'nate', 'password' => 'foo'),
+ ),
+ array(
+ 'Post' => array('id' => '2', 'author_id' => '3', 'title' => 'Second Post', 'body' => 'Second Post Body'),
+ 'Author' => array('id' => '3', 'user' => 'larry', 'password' => null),
+ )
+ );
+
+ $expected = array(
+ '0.Post.id' => '1', '0.Post.author_id' => '1', '0.Post.title' => 'First Post', '0.Author.id' => '1',
+ '0.Author.user' => 'nate', '0.Author.password' => 'foo', '1.Post.id' => '2', '1.Post.author_id' => '3',
+ '1.Post.title' => 'Second Post', '1.Post.body' => 'Second Post Body', '1.Author.id' => '3',
+ '1.Author.user' => 'larry', '1.Author.password' => null
+ );
+ $result = Set::flatten($data);
+ $this->assertEqual($expected, $result);
+
+ $result = Set::flatten(array('Post' => $data[0]['Post']), '/');
+ $expected = array('Post/id' => '1', 'Post/author_id' => '1', 'Post/title' => 'First Post');
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * testFilter method
+ *
+ * @return void
+ */
+ public function testFilter() {
+ $expected = array('one');
+ $result = Set::filter('one');
+ $this->assertIdentical($expected, $result);
+
+ $expected = array('0', 2 => true, 3 => 0, 4 => array('one thing', 'I can tell you', 'is you got to be', false));
+ $result = Set::filter(array('0', false, true, 0, array('one thing', 'I can tell you', 'is you got to be', false)));
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testFormatmethod
+ *
+ * @return void
+ */
+ public function testFormat() {
+ $data = array(
+ array('Person' => array('first_name' => 'Nate', 'last_name' => 'Abele', 'city' => 'Boston', 'state' => 'MA', 'something' => '42')),
+ array('Person' => array('first_name' => 'Larry', 'last_name' => 'Masters', 'city' => 'Boondock', 'state' => 'TN', 'something' => '{0}')),
+ array('Person' => array('first_name' => 'Garrett', 'last_name' => 'Woodworth', 'city' => 'Venice Beach', 'state' => 'CA', 'something' => '{1}')));
+
+ $result = Set::format($data, '{1}, {0}', array('/Person/first_name', '/Person/last_name'));
+ $expected = array('Abele, Nate', 'Masters, Larry', 'Woodworth, Garrett');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '{0}, {1}', array('/Person/last_name', '/Person/first_name'));
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '{0}, {1}', array('/Person/city', '/Person/state'));
+ $expected = array('Boston, MA', 'Boondock, TN', 'Venice Beach, CA');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '{{0}, {1}}', array('/Person/city', '/Person/state'));
+ $expected = array('{Boston, MA}', '{Boondock, TN}', '{Venice Beach, CA}');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '{{0}, {1}}', array('/Person/something', '/Person/something'));
+ $expected = array('{42, 42}', '{{0}, {0}}', '{{1}, {1}}');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '{%2$d, %1$s}', array('/Person/something', '/Person/something'));
+ $expected = array('{42, 42}', '{0, {0}}', '{0, {1}}');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '{%1$s, %1$s}', array('/Person/something', '/Person/something'));
+ $expected = array('{42, 42}', '{{0}, {0}}', '{{1}, {1}}');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '%2$d, %1$s', array('/Person/first_name', '/Person/something'));
+ $expected = array('42, Nate', '0, Larry', '0, Garrett');
+ $this->assertEqual($expected, $result);
+
+ $result = Set::format($data, '%1$s, %2$d', array('/Person/first_name', '/Person/something'));
+ $expected = array('Nate, 42', 'Larry, 0', 'Garrett, 0');
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * testMatches first, because extract and others depend on it.
+ *
+ * @return void
+ */
+ public function testMatchesBasic() {
+ $a = array(
+ array('Article' => array('id' => 1, 'title' => 'Article 1')),
+ array('Article' => array('id' => 2, 'title' => 'Article 2')),
+ 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));
+ }
+ /**
+ * testDeeperMatches method
+ *
+ * @return void
+ */
+ public function testMatchesMultipleLevels() {
+ $result = array(
+ 'Attachment' => array(
+ 'keep' => array()
+ ),
+ 'Comment' => array(
+ 'keep' => array('Attachment' => array('fields' => array('attachment')))
+ ),
+ 'User' => array('keep' => array()),
+ 'Article' => array(
+ 'keep' => array(
+ 'Comment' => array('fields' => array('comment', 'published')),
+ 'User' => array('fields' => array('user')),
+ )
+ )
+ );
+ $result = Set::matches($result, '/Article/keep/Comment');
+ $this->assertTrue($result);
+
+ $result = Set::matches($result, '/Article/keep/Comment/fields/user');
+ $this->assertFalse($result);
+ }
+
+ /**
+ * testSetExtractReturnsEmptyArray method
+ *
+ * @return void
+ */
+ public function testSetExtractReturnsEmptyArray() {
+ $expected = array();
+ $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'))
+ ), '/Post/id');
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::extract(array(), 'Message.flash');
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testNumericKeyExtraction method
+ *
+ * @return void
+ */
+ public function testExtractionOfNotNull() {
+ $data = array(
+ 'plugin' => null, 'admin' => false, 'controller' => 'posts',
+ 'action' => 'index', 1, 'whatever'
+ );
+
+ $expected = array('controller' => 'posts', 'action' => 'index', 1, 'whatever');
+ $result = Set::extract($data, '/');
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testNumericKeyExtraction method
+ *
+ * @return void
+ */
+ public function testNumericKeyExtraction() {
+ $data = array(1, 'whatever');
+
+ $expected = array(1, 'whatever');
+ $result = Set::extract($data, '/');
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testExtract method
+ *
+ * @return void
+ */
+ public function testExtract() {
+ $a = array(
+ array(
+ 'Article' => array('id' => '1', 'user_id' => '1', 'title' => 'First Article', 'body' => 'First Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:39:23', 'updated' => '2007-03-18 10:41:31'),
+ 'User' => array(
+ 'id' => '1', 'user' => 'mariano',
+ 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
+ 'created' => '2007-03-17 01:16:23',
+ 'updated' => '2007-03-17 01:18:31'
+ ),
+ 'Comment' => array(
+ array(
+ 'id' => '1', 'article_id' => '1', 'user_id' => '2',
+ 'comment' => 'First Comment for First Article',
+ 'published' => 'Y', 'created' => '2007-03-18 10:45:23',
+ 'updated' => '2007-03-18 10:47:31'
+ ),
+ array(
+ 'id' => '2', 'article_id' => '1', 'user_id' => '4',
+ 'comment' => 'Second Comment for First Article', 'published' => 'Y',
+ 'created' => '2007-03-18 10:47:23',
+ 'updated' => '2007-03-18 10:49:31'
+ ),
+ ),
+ 'Tag' => array(
+ array(
+ 'id' => '1', 'tag' => 'tag1', 'created' => '2007-03-18 12:22:23',
+ 'updated' => '2007-03-18 12:24:31'
+ ),
+ array(
+ 'id' => '2', 'tag' => 'tag2', 'created' => '2007-03-18 12:24:23',
+ 'updated' => '2007-03-18 12:26:31'
+ )
+ ),
+ 'Deep' => array(
+ 'Nesting' => array(
+ 'test' => array(1 => 'foo', 2 => array('and' => array('more' => 'stuff')))
+ )
+ )
+ ),
+ array(
+ 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'),
+ 'User' => array('id' => '2', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'),
+ 'Comment' => array(),
+ 'Tag' => array()
+ ),
+ array(
+ 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'),
+ 'User' => array('id' => '3', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'),
+ 'Comment' => array(),
+ 'Tag' => array()
+ ),
+ array(
+ 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'),
+ 'User' => array('id' => '4', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'),
+ 'Comment' => array(),
+ 'Tag' => array()
+ ),
+ array(
+ 'Article' => array('id' => '3', 'user_id' => '1', 'title' => 'Third Article', 'body' => 'Third Article Body', 'published' => 'Y', 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'),
+ 'User' => array('id' => '5', 'user' => 'mariano', 'password' => '5f4dcc3b5aa765d61d8327deb882cf99', 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'),
+ 'Comment' => array(),
+ 'Tag' => array()
+ )
+ );
+
+ $b = array('Deep' => $a[0]['Deep']);
+
+ $c = array(
+ array('a' => array('I' => array('a' => 1))),
+ array('a' => array(2)),
+ array('a' => array('II' => array('a' => 3, 'III' => array('a' => array('foo' => 4))))),
+ );
+
+ $expected = array(array('a' => $c[2]['a']));
+ $result = Set::extract('/a/II[a=3]/..', $c);
+ $this->assertEqual($expected, $result);
+
+ $expected = array(1, 2, 3, 4, 5);
+ $result = Set::extract($a, '/User/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(1, 2, 3, 4, 5);
+ $result = Set::extract($a, '/User/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(
+ array('id' => 1), array('id' => 2), array('id' => 3), array('id' => 4), array('id' => 5)
+ );
+
+ $result = Set::extract($a, '/User/id', array('flatten' => false));
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array('test' => $a[0]['Deep']['Nesting']['test']));
+ $this->assertEqual(Set::extract($a, '/Deep/Nesting/test'), $expected);
+ $this->assertEqual(Set::extract($b, '/Deep/Nesting/test'), $expected);
+
+ $expected = array(array('test' => $a[0]['Deep']['Nesting']['test']));
+ $result = Set::extract($a, '/Deep/Nesting/test/1/..');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array('test' => $a[0]['Deep']['Nesting']['test']));
+ $result = Set::extract($a, '/Deep/Nesting/test/2/and/../..');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array('test' => $a[0]['Deep']['Nesting']['test']));
+ $result = Set::extract($a, '/Deep/Nesting/test/2/../../../Nesting/test/2/..');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(2);
+ $result = Set::extract($a, '/User[2]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(4, 5);
+ $result = Set::extract($a, '/User[id>3]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(2, 3);
+ $result = Set::extract($a, '/User[id>1][id<=3]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array('I'), array('II'));
+ $result = Set::extract($c, '/a/@*');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractWithNonSequentialKeys() {
+ $nonSequential = array(
+ 'User' => array(
+ 0 => array('id' => 1),
+ 2 => array('id' => 2),
+ 6 => array('id' => 3),
+ 9 => array('id' => 4),
+ 3 => array('id' => 5),
+ ),
+ );
+
+ $expected = array(1, 2, 3, 4, 5);
+ $result = Set::extract($nonSequential, '/User/id');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractWithNoZeroKey() {
+ $noZero = array(
+ 'User' => array(
+ 2 => array('id' => 1),
+ 4 => array('id' => 2),
+ 6 => array('id' => 3),
+ 9 => array('id' => 4),
+ 3 => array('id' => 5),
+ ),
+ );
+
+ $expected = array(1, 2, 3, 4, 5);
+ $result = Set::extract($noZero, '/User/id');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractSingle() {
+
+ $single = array('User' => array('id' => 4, 'name' => 'Neo'));
+
+ $expected = array(4);
+ $result = Set::extract($single, '/User/id');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractHasMany() {
+ $tricky = array(
+ 0 => array('User' => array('id' => 1, 'name' => 'John')),
+ 1 => array('User' => array('id' => 2, 'name' => 'Bob')),
+ 2 => array('User' => array('id' => 3, 'name' => 'Tony')),
+ 'User' => array('id' => 4, 'name' => 'Neo')
+ );
+
+ $expected = array(1, 2, 3, 4);
+ $result = Set::extract($tricky, '/User/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(1, 3);
+ $result = Set::extract($tricky, '/User[name=/n/]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(4);
+ $result = Set::extract($tricky, '/User[name=/N/]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(1, 3, 4);
+ $result = Set::extract($tricky, '/User[name=/N/i]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(
+ array('id', 'name'), array('id', 'name'), array('id', 'name'), array('id', 'name')
+ );
+ $result = Set::extract($tricky, '/User/@*');
+ $this->assertEqual($expected, $result);
+ }
+
+ function testExtractAssociatedHasMany() {
+ $common = array(
+ array(
+ 'Article' => array('id' => 1, 'name' => 'Article 1'),
+ 'Comment' => array(
+ array('id' => 1, 'user_id' => 5, 'article_id' => 1, 'text' => 'Comment 1'),
+ array('id' => 2, 'user_id' => 23, 'article_id' => 1, 'text' => 'Comment 2'),
+ array('id' => 3, 'user_id' => 17, 'article_id' => 1, 'text' => 'Comment 3')
+ )
+ ),
+ array(
+ 'Article' => array('id' => 2, 'name' => 'Article 2'),
+ 'Comment' => array(
+ array(
+ 'id' => 4,
+ 'user_id' => 2,
+ 'article_id' => 2,
+ 'text' => 'Comment 4',
+ 'addition' => ''
+ ),
+ array(
+ 'id' => 5,
+ 'user_id' => 23,
+ 'article_id' => 2,
+ 'text' => 'Comment 5',
+ 'addition' => 'foo'
+ ),
+ ),
+ ),
+ array(
+ 'Article' => array('id' => 3, 'name' => 'Article 3'),
+ 'Comment' => array()
+ )
+ );
+ $result = Set::extract('/', $common);
+ $this->assertEqual($result, $common);
+
+ $expected = array(1);
+ $result = Set::extract('/Comment/id[:first]', $common);
+ $this->assertEqual($expected, $result);
+
+ $expected = array(5);
+ $result = Set::extract('/Comment/id[:last]', $common);
+ $this->assertEqual($expected, $result);
+
+ $result = Set::extract($common, '/Comment/id');
+ $expected = array(1, 2, 3, 4, 5);
+ $this->assertEqual($expected, $result);
+
+ $expected = array(1, 2, 4, 5);
+ $result = Set::extract($common, '/Comment[id!=3]/id');
+ $this->assertEqual($expected, $result);
+
+ $expected = array($common[0]['Comment'][2]);
+ $result = Set::extract($common, '/Comment/2');
+ $this->assertEqual($expected, $result);
+
+ $expected = array($common[0]['Comment'][0]);
+ $result = Set::extract($common, '/Comment[1]/.[id=1]');
+ $this->assertEqual($expected, $result);
+
+ $expected = array($common[1]['Comment'][1]);
+ $result = Set::extract($common, '/1/Comment/.[2]');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array('Comment' => $common[1]['Comment'][0]));
+ $result = Set::extract('/Comment[addition=]', $common);
+ $this->assertEqual($expected, $result);
+
+ $expected = array(3);
+ $result = Set::extract('/Article[:last]/id', $common);
+ $this->assertEqual($expected, $result);
+
+ $expected = array();
+ $result = Set::extract('/User/id', array());
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractHabtm() {
+ $habtm = array(
+ array(
+ 'Post' => array('id' => 1, 'title' => 'great post'),
+ 'Comment' => array(
+ array('id' => 1, 'text' => 'foo', 'User' => array('id' => 1, 'name' => 'bob')),
+ array('id' => 2, 'text' => 'bar', 'User' => array('id' => 2, 'name' => 'tod')),
+ ),
+ ),
+ array(
+ 'Post' => array('id' => 2, 'title' => 'fun post'),
+ 'Comment' => array(
+ array('id' => 3, 'text' => '123', 'User' => array('id' => 3, 'name' => 'dan')),
+ array('id' => 4, 'text' => '987', 'User' => array('id' => 4, 'name' => 'jim'))
+ )
+ )
+ );
+
+ $result = Set::extract($habtm, '/Comment/User[name=/\w+/]/..');
+ $this->assertEqual(count($result), 4);
+ $this->assertEqual($result[0]['Comment']['User']['name'], 'bob');
+ $this->assertEqual($result[1]['Comment']['User']['name'], 'tod');
+ $this->assertEqual($result[2]['Comment']['User']['name'], 'dan');
+ $this->assertEqual($result[3]['Comment']['User']['name'], 'dan');
+
+ $result = Set::extract($habtm, '/Comment/User[name=/[a-z]+/]/..');
+ $this->assertEqual(count($result), 4);
+ $this->assertEqual($result[0]['Comment']['User']['name'], 'bob');
+ $this->assertEqual($result[1]['Comment']['User']['name'], 'tod');
+ $this->assertEqual($result[2]['Comment']['User']['name'], 'dan');
+ $this->assertEqual($result[3]['Comment']['User']['name'], 'dan');
+
+ $result = Set::extract($habtm, '/Comment/User[name=/bob|dan/]/..');
+ $this->assertEqual(count($result), 2);
+ $this->assertEqual($result[0]['Comment']['User']['name'], 'bob');
+ $this->assertEqual($result[1]['Comment']['User']['name'], 'dan');
+
+ $result = Set::extract($habtm, '/Comment/User[name=/bob|tod/]/..');
+ $this->assertEqual(count($result), 2);
+ $this->assertEqual($result[0]['Comment']['User']['name'], 'bob');
+ $this->assertEqual($result[1]['Comment']['User']['name'], 'tod');
+ }
+
+ public function testExtractFromTree() {
+ $tree = array(
+ array(
+ 'Category' => array('name' => 'Category 1'),
+ 'children' => array(array('Category' => array('name' => 'Category 1.1')))
+ ),
+ array(
+ 'Category' => array('name' => 'Category 2'),
+ 'children' => array(
+ array('Category' => array('name' => 'Category 2.1')),
+ array('Category' => array('name' => 'Category 2.2'))
+ )
+ ),
+ array(
+ 'Category' => array('name' => 'Category 3'),
+ 'children' => array(array('Category' => array('name' => 'Category 3.1')))
+ )
+ );
+
+ $expected = array(array('Category' => $tree[1]['Category']));
+ $result = Set::extract($tree, '/Category[name=Category 2]');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array('Category' => $tree[1]['Category'], 'children' => $tree[1]['children']));
+ $result = Set::extract($tree, '/Category[name=Category 2]/..');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(
+ array('children' => $tree[1]['children'][0]),
+ array('children' => $tree[1]['children'][1])
+ );
+ $result = Set::extract($tree, '/Category[name=Category 2]/../children');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractOnMixedKeys() {
+ $mixedKeys = array(
+ 'User' => array(
+ 0 => array('id' => 4, 'name' => 'Neo'),
+ 1 => array('id' => 5, 'name' => 'Morpheus'),
+ 'stringKey' => array()
+ )
+ );
+
+ $expected = array('Neo', 'Morpheus');
+ $result = Set::extract($mixedKeys, '/User/name');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractSingleWithNameCondition() {
+ $single = array(
+ array('CallType' => array('name' => 'Internal Voice'), 'x' => array('hour' => 7))
+ );
+
+ $expected = array(7);
+ $result = Set::extract($single, '/CallType[name=Internal Voice]/../x/hour');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractWithNameCondition() {
+ $multiple = array(
+ array('CallType' => array('name' => 'Internal Voice'), 'x' => array('hour' => 7)),
+ array('CallType' => array('name' => 'Internal Voice'), 'x' => array('hour' => 2)),
+ array('CallType' => array('name' => 'Internal Voice'), 'x' => array('hour' => 1))
+ );
+
+ $expected = array(7, 2, 1);
+ $result = Set::extract($multiple, '/CallType[name=Internal Voice]/../x/hour');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testExtractWithTypeCondition() {
+ $f = array(
+ array(
+ 'file' => array(
+ 'name' => 'zipfile.zip',
+ 'type' => 'application/zip',
+ 'tmp_name' => '/tmp/php178.tmp',
+ 'error' => 0,
+ 'size' => '564647'
+ )
+ ),
+ array(
+ 'file' => array(
+ 'name' => 'zipfile2.zip',
+ 'type' => 'application/x-zip-compressed',
+ 'tmp_name' => '/tmp/php179.tmp',
+ 'error' => 0,
+ 'size' => '354784'
+ )
+ ),
+ array(
+ 'file' => array(
+ 'name' => 'picture.jpg',
+ 'type' => 'image/jpeg',
+ 'tmp_name' => '/tmp/php180.tmp',
+ 'error' => 0,
+ 'size' => '21324'
+ )
+ )
+ );
+ $expected = array(array(
+ 'name' => 'zipfile2.zip', 'type' => 'application/x-zip-compressed',
+ 'tmp_name' => '/tmp/php179.tmp', 'error' => 0, 'size' => '354784'
+ ));
+ $result = Set::extract($f, '/file/.[type=application/x-zip-compressed]');
+ $this->assertEqual($expected, $result);
+
+ $expected = array(array(
+ 'name' => 'zipfile.zip', 'type' => 'application/zip',
+ 'tmp_name' => '/tmp/php178.tmp', 'error' => 0, 'size' => '564647'
+ ));
+ $result = Set::extract($f, '/file/.[type=application/zip]');
+ $this->assertEqual($expected, $result);
+
+ }
+
+ /**
+ * testNumericArrayCheck method
+ *
+ * @return void
+ */
+ public function testNumericArrayCheck() {
+ $data = array('one');
+ $this->assertTrue(Set::isNumeric(array_keys($data)));
+
+ $data = array(1 => 'one');
+ $this->assertFalse(Set::isNumeric($data));
+
+ $data = array('one');
+ $this->assertFalse(Set::isNumeric($data));
+
+ $data = array('one' => 'two');
+ $this->assertFalse(Set::isNumeric($data));
+
+ $data = array('one' => 1);
+ $this->assertTrue(Set::isNumeric($data));
+
+ $data = array(0);
+ $this->assertTrue(Set::isNumeric($data));
+
+ $data = array('one', 'two', 'three', 'four', 'five');
+ $this->assertTrue(Set::isNumeric(array_keys($data)));
+
+ $data = array(1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five');
+ $this->assertTrue(Set::isNumeric(array_keys($data)));
+
+ $data = array('1' => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five');
+ $this->assertTrue(Set::isNumeric(array_keys($data)));
+
+ $data = array('one', 2 => 'two', 3 => 'three', 4 => 'four', 'a' => 'five');
+ $this->assertFalse(Set::isNumeric(array_keys($data)));
+ }
+
+ /**
+ * testKeyCheck method
+ *
+ * @return void
+ */
+ public function testKeyCheck() {
+ $data = array('Multi' => array('dimensonal' => array('array')));
+ $this->assertTrue(Set::check($data, 'Multi.dimensonal'));
+ $this->assertFalse(Set::check($data, 'Multi.dimensonal.array'));
+
+ $data = array(
+ array(
+ 'Article' => array(
+ 'id' => '1', 'user_id' => '1', 'title' => 'First Article',
+ 'body' => 'First Article Body', 'published' => 'Y',
+ 'created' => '2007-03-18 10:39:23',
+ 'updated' => '2007-03-18 10:41:31'
+ ),
+ 'User' => array(
+ 'id' => '1', 'user' => 'mariano',
+ 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
+ 'created' => '2007-03-17 01:16:23',
+ 'updated' => '2007-03-17 01:18:31'
+ ),
+ 'Comment' => array(
+ array(
+ 'id' => '1', 'article_id' => '1', 'user_id' => '2',
+ 'comment' => 'First Comment for First Article',
+ 'published' => 'Y', 'created' => '2007-03-18 10:45:23',
+ 'updated' => '2007-03-18 10:47:31'
+ ),
+ array(
+ 'id' => '2', 'article_id' => '1', 'user_id' => '4',
+ 'comment' => 'Second Comment for First Article',
+ 'published' => 'Y', 'created' => '2007-03-18 10:47:23',
+ 'updated' => '2007-03-18 10:49:31'
+ ),
+ ),
+ 'Tag' => array(
+ array(
+ 'id' => '1', 'tag' => 'tag1', 'created' => '2007-03-18 12:22:23',
+ 'updated' => '2007-03-18 12:24:31'
+ ),
+ array(
+ 'id' => '2', 'tag' => 'tag2', 'created' => '2007-03-18 12:24:23',
+ 'updated' => '2007-03-18 12:26:31'
+ )
+ )
+ ),
+ array(
+ 'Article' => array(
+ 'id' => '3', 'user_id' => '1', 'title' => 'Third Article',
+ 'body' => 'Third Article Body', 'published' => 'Y',
+ 'created' => '2007-03-18 10:43:23', 'updated' => '2007-03-18 10:45:31'
+ ),
+ 'User' => array(
+ 'id' => '1', 'user' => 'mariano',
+ 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
+ 'created' => '2007-03-17 01:16:23', 'updated' => '2007-03-17 01:18:31'
+ ),
+ 'Comment' => array(),
+ 'Tag' => array()
+ )
+ );
+ $this->assertTrue(Set::check($data, '0.Article.user_id'));
+ $this->assertTrue(Set::check($data, '0.Comment.0.id'));
+ $this->assertFalse(Set::check($data, '0.Comment.0.id.0'));
+ $this->assertTrue(Set::check($data, '0.Article.user_id'));
+ $this->assertFalse(Set::check($data, '0.Article.user_id.a'));
+ }
+
+ /**
+ * testMerge method
+ *
+ * @return void
+ */
+ public function testMerge() {
+ $result = Set::merge(array('foo'));
+ $this->assertIdentical($result, array('foo'));
+
+ $result = Set::merge('foo');
+ $this->assertIdentical($result, array('foo'));
+
+ $result = Set::merge('foo', 'bar');
+ $this->assertIdentical($result, array('foo', 'bar'));
+
+ $result = Set::merge('foo', array('user' => 'bob', 'no-bar'), 'bar');
+ $this->assertIdentical($result, array('foo', 'user' => 'bob', 'no-bar', 'bar'));
+
+ $a = array('foo', 'foo2');
+ $b = array('bar', 'bar2');
+ $this->assertIdentical(Set::merge($a, $b), array('foo', 'foo2', 'bar', 'bar2'));
+
+ $a = array('foo' => 'bar', 'bar' => 'foo');
+ $b = array('foo' => 'no-bar', 'bar' => 'no-foo');
+ $this->assertIdentical(Set::merge($a, $b), array('foo' => 'no-bar', 'bar' => 'no-foo'));
+
+ $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')));
+
+ $a = array('users' => array('jim', 'bob'));
+ $b = array('users' => 'none');
+ $this->assertIdentical(Set::merge($a, $b), array('users' => 'none'));
+
+ $a = array('users' => array('lisa' => array('id' => 5, 'pw' => 'secret')), 'lithiumphp');
+ $b = array('users' => array('lisa' => array('pw' => 'new-pass', 'age' => 23)), 'ice-cream');
+ $this->assertIdentical(Set::merge($a, $b), array('users' => array('lisa' => array('id' => 5, 'pw' => 'new-pass', 'age' => 23)), 'lithiumphp', 'ice-cream'));
+
+ $c = array('users' => array('lisa' => array('pw' => 'you-will-never-guess', 'age' => 25, 'pet' => 'dog')), 'chocolate');
+ $expected = array('users' => array('lisa' => array('id' => 5, 'pw' => 'you-will-never-guess', 'age' => 25, 'pet' => 'dog')), 'lithiumphp', 'ice-cream', 'chocolate');
+ $this->assertIdentical(Set::merge($a, $b, $c), $expected);
+
+ $this->assertIdentical(Set::merge($a, $b, array(), $c), $expected);
+
+ $result = Set::merge($a, $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');
+
+ $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);
+
+ $this->assertIdentical(Set::normalize(Set::merge($a, $b)), $expected);
+ }
+
+
+ /**
+ * testSort depends on extract
+ *
+ * @return void
+ */
+ 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')))
+ );
+ $b = array(
+ 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');
+ $this->assertIdentical($a, $b);
+
+ $b = array(
+ array('Person' => array('name' => 'Jeff'), 'Friend' => array(array('name' => 'Nate'))),
+ 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' => 'Jeff'), 'Friend' => array(array('name' => 'Nate')))
+ );
+ $a = Set::sort($a, '/Friend/name', 'desc');
+ $this->assertIdentical($a, $b);
+
+ $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' => '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')))
+ );
+ $a = Set::sort($a, '/Person/name', 'asc');
+ $this->assertIdentical($a, $b);
+
+ $a = array(array(7, 6, 4), array(3, 4, 5), array(3, 2, 1));
+ $b = array(array(3, 2, 1), array(3, 4, 5), array(7, 6, 4));
+
+ $a = Set::sort($a, '/', 'asc');
+ $this->assertIdentical($a, $b);
+
+ $a = array(array(7, 6, 4), array(3, 4, 5), array(3, 2, array(1, 1, 1)));
+ $b = array(array(3, 2, array(1, 1, 1)), array(3, 4, 5), array(7, 6, 4));
+
+ $a = Set::sort($a, '/.', 'asc');
+ $this->assertIdentical($a, $b);
+
+ $a = array(
+ array('Person' => array('name' => 'Jeff')),
+ array('Shirt' => array('color' => 'black'))
+ );
+ $b = array(array('Person' => array('name' => 'Jeff')));
+ $a = Set::sort($a, '/Person/name', 'asc');
+ $this->assertIdentical($a, $b);
+ }
+
+ /**
+ * testInsert method
+ *
+ * @return void
+ */
+ public function testInsert() {
+ $a = array('pages' => array('name' => 'page'));
+
+ $result = Set::insert($a, 'files', array('name' => 'files'));
+ $expected = array('pages' => array('name' => 'page'), 'files' => array('name' => 'files'));
+ $this->assertIdentical($expected, $result);
+
+ $a = array('pages' => array('name' => 'page'));
+ $result = Set::insert($a, 'pages.name', array());
+ $expected = array('pages' => array('name' => array()));
+ $this->assertIdentical($expected, $result);
+
+ $a = array('pages' => array(array('name' => 'main'), array('name' => 'about')));
+
+ $result = Set::insert($a, 'pages.1.vars', array('title' => 'page title'));
+ $expected = array(
+ 'pages' => array(
+ array('name' => 'main'),
+ array('name' => 'about', 'vars' => array('title' => 'page title'))
+ )
+ );
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testRemove method
+ *
+ * @return void
+ */
+ public function testRemove() {
+ $a = array('pages' => array('name' => 'page'), 'files' => array('name' => 'files'));
+
+ $result = Set::remove($a, 'files', array('name' => 'files'));
+ $expected = array('pages' => array('name' => 'page'));
+ $this->assertIdentical($expected, $result);
+
+ $a = array(
+ 'pages' => array(
+ array('name' => 'main'),
+ array('name' => 'about', 'vars' => array('title' => 'page title'))
+ )
+ );
+
+ $result = Set::remove($a, 'pages.1.vars', array('title' => 'page title'));
+ $expected = array('pages' => array(array('name' => 'main'), array('name' => 'about')));
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::remove($a, 'pages.2.vars', array('title' => 'page title'));
+ $expected = $a;
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testCheck method
+ *
+ * @return void
+ */
+ public function testCheck() {
+ $set = array(
+ 'My Index 1' => array('First' => 'The first item')
+ );
+ $this->assertTrue(Set::check($set, 'My Index 1.First'));
+ $this->assertTrue(Set::check($set, 'My Index 1'));
+ $this->assertTrue(Set::check($set, array()));
+
+ $set = array('My Index 1' => array('First' => array('Second' => array('Third' => array(
+ 'Fourth' => 'Heavy. Nesting.'
+ )))));
+ $this->assertTrue(Set::check($set, 'My Index 1.First.Second'));
+ $this->assertTrue(Set::check($set, 'My Index 1.First.Second.Third'));
+ $this->assertTrue(Set::check($set, 'My Index 1.First.Second.Third.Fourth'));
+ $this->assertFalse(Set::check($set, 'My Index 1.First.Seconds.Third.Fourth'));
+ }
+
+ /**
+ * testWritingWithFunkyKeys method
+ *
+ * @return void
+ */
+ public function testWritingWithFunkyKeys() {
+ $set = Set::insert(array(), 'Session Test', "test");
+ $result = Set::extract($set, '/Session Test');
+ $this->assertEqual($result, array('test'));
+
+ $set = Set::remove($set, 'Session Test');
+ $this->assertFalse(Set::check($set, 'Session Test'));
+
+ $this->assertTrue($set = Set::insert(array(), 'Session Test.Test Case', "test"));
+ $this->assertTrue(Set::check($set, 'Session Test.Test Case'));
+ }
+
+ /**
+ * testDiff method
+ *
+ * @return void
+ */
+ public function testDiff() {
+ $a = array(array('name' => 'main'), array('name' => 'about'));
+ $b = array(array('name' => 'main'), array('name' => 'about'), array('name' => 'contact'));
+
+ $result = Set::diff($a, $b);
+ $expected = array(2 => array('name' => 'contact'));
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::diff($a, array());
+ $expected = $a;
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::diff(array(), $b);
+ $expected = $b;
+ $this->assertIdentical($expected, $result);
+
+ $b = array(array('name' => 'me'), array('name' => 'about'));
+
+ $result = Set::diff($a, $b);
+ $expected = array(array('name' => 'main'));
+ $this->assertIdentical($expected, $result);
+ }
+
+ /**
+ * testContains method
+ *
+ * @return void
+ */
+ public function testContains() {
+ $a = array(
+ 0 => array('name' => 'main'),
+ 1 => array('name' => 'about')
+ );
+ $b = array(
+ 0 => array('name' => 'main'),
+ 1 => array('name' => 'about'),
+ 2 => array('name' => 'contact'),
+ 'a' => 'b'
+ );
+
+ $this->assertTrue(Set::contains($a, $a));
+ $this->assertFalse(Set::contains($a, $b));
+ $this->assertTrue(Set::contains($b, $a));
+ }
+ /**
+ * testCombine method
+ *
+ * @return void
+ */
+ public function testCombine() {
+ $result = Set::combine(array(), '/User/id', '/User/Data');
+ $this->assertFalse($result);
+ $result = Set::combine('', '/User/id', '/User/Data');
+ $this->assertFalse($result);
+
+ $a = array(
+ array('User' => array('id' => 2, 'group_id' => 1,
+ 'Data' => array('user' => 'mariano.iglesias','name' => 'Mariano Iglesias'))),
+ array('User' => array('id' => 14, 'group_id' => 2,
+ 'Data' => array('user' => 'phpnut', 'name' => 'Larry E. Masters'))),
+ array('User' => array('id' => 25, 'group_id' => 1,
+ 'Data' => array('user' => 'gwoo','name' => 'The Gwoo'))));
+ $result = Set::combine($a, '/User/id');
+ $expected = array(2 => null, 14 => null, 25 => null);
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, '/User/id', '/User/non-existant');
+ $expected = array(2 => null, 14 => null, 25 => null);
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, '/User/id', '/User/Data/.');
+ $expected = array(
+ 2 => array('user' => 'mariano.iglesias', 'name' => 'Mariano Iglesias'),
+ 14 => array('user' => 'phpnut', 'name' => 'Larry E. Masters'),
+ 25 => array('user' => 'gwoo', 'name' => 'The Gwoo'));
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, '/User/id', '/User/Data/name/.');
+ $expected = array(
+ 2 => 'Mariano Iglesias',
+ 14 => 'Larry E. Masters',
+ 25 => 'The Gwoo');
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, '/User/id', '/User/Data/.', '/User/group_id');
+ $expected = array(
+ 1 => array(
+ 2 => array('user' => 'mariano.iglesias', 'name' => 'Mariano Iglesias'),
+ 25 => array('user' => 'gwoo', 'name' => 'The Gwoo')),
+ 2 => array(
+ 14 => array('user' => 'phpnut', 'name' => 'Larry E. Masters')));
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, '/User/id', '/User/Data/name/.', '/User/group_id');
+ $expected = array(
+ 1 => array(
+ 2 => 'Mariano Iglesias',
+ 25 => 'The Gwoo'),
+ 2 => array(
+ 14 => 'Larry E. Masters'));
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, '/User/id', array('{0}: {1}', '/User/Data/user', '/User/Data/name'), '/User/group_id');
+ $expected = array(
+ 1 => array(2 => 'mariano.iglesias: Mariano Iglesias', 25 => 'gwoo: The Gwoo'),
+ 2 => array(14 => 'phpnut: Larry E. Masters')
+ );
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, array('{0}: {1}', '/User/Data/user', '/User/Data/name'), '/User/id');
+ $expected = array('mariano.iglesias: Mariano Iglesias' => 2, 'phpnut: Larry E. Masters' => 14, 'gwoo: The Gwoo' => 25);
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, array('{1}: {0}', '/User/Data/user', '/User/Data/name'), '/User/id');
+ $expected = array('Mariano Iglesias: mariano.iglesias' => 2, 'Larry E. Masters: phpnut' => 14, 'The Gwoo: gwoo' => 25);
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, array('%1$s: %2$d', '/User/Data/user', '/User/id'), '/User/Data/name');
+ $expected = array('mariano.iglesias: 2' => 'Mariano Iglesias', 'phpnut: 14' => 'Larry E. Masters', 'gwoo: 25' => 'The Gwoo');
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($a, array('%2$d: %1$s', '/User/Data/user', '/User/id'), '/User/Data/name');
+ $expected = array('2: mariano.iglesias' => 'Mariano Iglesias', '14: phpnut' => 'Larry E. Masters', '25: gwoo' => 'The Gwoo');
+ $this->assertIdentical($expected, $result);
+
+ $b = new \stdClass();
+ $b->users = array(
+ array('User' => array(
+ 'id' => 2, 'group_id' => 1, 'Data' => array(
+ 'user' => 'mariano.iglesias','name' => 'Mariano Iglesias'
+ )
+ )),
+ array('User' => array('id' => 14, 'group_id' => 2, 'Data' => array(
+ 'user' => 'phpnut', 'name' => 'Larry E. Masters'
+ ))),
+ array('User' => array('id' => 25, 'group_id' => 1, 'Data' => array(
+ 'user' => 'gwoo','name' => 'The Gwoo'
+ )))
+ );
+ $result = Set::combine($b, '/users/User/id');
+ $expected = array(2 => null, 14 => null, 25 => null);
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::combine($b, '/users/User/id', '/users/User/non-existant');
+ $expected = array(2 => null, 14 => null, 25 => null);
+ $this->assertIdentical($expected, $result);
+ }
+ /**
+ * testMapReverse method
+ *
+ * @return void
+ */
+ public function testMapReverse() {
+ $result = Set::reverse(null);
+ $this->assertEqual($result, null);
+
+ $result = Set::reverse(false);
+ $this->assertEqual($result, false);
+
+ $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::map($expected, true);
+ $this->assertEqual($map->Array1->Array1Data1, $expected['Array1']['Array1Data1']);
+ $this->assertEqual($map->Array2[0]->Array2Data1, $expected['Array2'][0]['Array2Data1']);
+
+ $result = Set::reverse($map);
+ $this->assertEqual($expected, $result);
+
+ $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::map($expected);
+ $this->assertIdentical($map->title, $expected['Post']['title']);
+ foreach ($map->Comment as $comment) {
+ $ids[] = $comment->id;
+ }
+ $this->assertIdentical($ids, array(1, 2));
+
+ $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::map($expected, true);
+ $result = Set::reverse($map);
+ $this->assertIdentical($expected, $result);
+
+ $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')
+ ),
+ 'string2' => 1,
+ 'another2' => 'string',
+ 'some2' => 'thing else',
+ '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')
+ ),
+ 'string3' => 1,
+ 'another3' => 'string',
+ 'some3' => 'thing else'
+ );
+ $map = Set::map($expected, true);
+ $result = Set::reverse($map);
+ $this->assertIdentical($expected, $result);
+
+ $expected = array('User' => array('psword'=> 'whatever', 'Icon' => array('id' => 851)));
+ $map = Set::map($expected);
+ $result = Set::reverse($map);
+ $this->assertIdentical($expected, $result);
+
+ $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::reverse($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::reverse($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::reverse($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::reverse($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::reverse($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::reverse($class);
+ $expected = array(
+ 'User' => array('id' => '100'),
+ 'Profile' => array('name' => 'Joe Mamma')
+ );
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * testMapNesting method
+ *
+ * @return void
+ */
+ public function testMapNesting() {
+ $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::map($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::reverse($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::map($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));
+
+ $result = Set::map(null);
+ $expected = null;
+ $this->assertEqual($expected, $result);
+ }
+ /**
+ * testNestedMappedData method
+ *
+ * @return void
+ */
+ public function testNestedMappedData() {
+ $result = Set::map(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' => 'larry',
+ 'password' => '5f4dcc3b5aa765d61d8327deb882cf99',
+ 'created' => '2007-03-17 01:20:23',
+ 'updated' => '2007-03-17 01:22:31',
+ 'test' => 'working'
+ ),
+ )
+ ));
+
+ $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 = 'larry';
+ $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);
+
+ $result = Set::map(
+ 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'
+ ),
+ )
+ );
+ $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);
+
+ //Case where extra HABTM fields come back in a result
+ $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::map($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);
+
+ //Same data, but should work if _name_ has been manually defined:
+ $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::map($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);
+ }
+ /**
+ * testPushDiff method
+ *
+ * @return void
+ */
+ public function testPushDiff() {
+ $array1 = array('ModelOne' => array(
+ 'id' => 1001, 'field_one' => 'a1.m1.f1', 'field_two' => 'a1.m1.f2'
+ ));
+ $array2 = array('ModelTwo' => array(
+ 'id' => 1002, 'field_one' => 'a2.m2.f1', 'field_two' => 'a2.m2.f2'
+ ));
+
+ $result = Set::pushDiff($array1, $array2);
+
+ $this->assertIdentical($result, $array1 + $array2);
+
+ $array3 = array('ModelOne' => array(
+ 'id' => 1003, 'field_one' => 'a3.m1.f1',
+ 'field_two' => 'a3.m1.f2', 'field_three' => 'a3.m1.f3'
+ ));
+ $result = Set::pushDiff($array1, $array3);
+
+ $expected = array('ModelOne' => array(
+ 'id' => 1001, 'field_one' => 'a1.m1.f1',
+ 'field_two' => 'a1.m1.f2', 'field_three' => 'a3.m1.f3'
+ ));
+ $this->assertIdentical($expected, $result);
+
+
+ $array1 = array(
+ array('ModelOne' => array(
+ 'id' => 1001, 'field_one' => 's1.0.m1.f1', 'field_two' => 's1.0.m1.f2'
+ )),
+ array('ModelTwo' => array(
+ 'id' => 1002, 'field_one' => 's1.1.m2.f2', 'field_two' => 's1.1.m2.f2'
+ ))
+ );
+ $array2 = array(
+ array('ModelOne' => array(
+ 'id' => 1001, 'field_one' => 's2.0.m1.f1', 'field_two' => 's2.0.m1.f2'
+ )),
+ array('ModelTwo' => array(
+ 'id' => 1002, 'field_one' => 's2.1.m2.f2', 'field_two' => 's2.1.m2.f2'
+ ))
+ );
+
+ $result = Set::pushDiff($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::pushDiff($array1, $array3);
+ $expected = array(
+ array(
+ 'ModelOne' => array(
+ 'id' => 1001, 'field_one' => 's1.0.m1.f1', 'field_two' => 's1.0.m1.f2'
+ ),
+ 'ModelThree' => array(
+ 'id' => 1003, 'field_one' => 's3.0.m3.f1', 'field_two' => 's3.0.m3.f2'
+ )
+ ),
+ array('ModelTwo' => array(
+ 'id' => 1002, 'field_one' => 's1.1.m2.f2', 'field_two' => 's1.1.m2.f2'
+ ))
+ );
+ $this->assertIdentical($expected, $result);
+
+ $result = Set::pushDiff($array1, null);
+ $this->assertIdentical($result, $array1);
+
+ $result = Set::pushDiff($array1, $array2);
+ $this->assertIdentical($result, $array1 + $array2);
+ }
+
+ /**
+ * testStrictKeyCheck method
+ *
+ * @return void
+ */
+ public function testStrictKeyCheck() {
+ $set = array('a' => 'hi');
+ $this->assertFalse(Set::check($set, 'a.b'));
+ }
+
+ /**
+ * Tests list normalization where the input array's keys are mixed between strings and integers.
+ *
+ * @return void
+ */
+ public function testMixedKeyNormalization() {
+ $input = array('"string"' => array('before' => '=>'), 1 => array('before' => '=>'));
+ $result = Set::normalize($input);
+ $this->assertEqual($input, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/StringTest.php b/libraries/lithium/tests/cases/util/StringTest.php
new file mode 100644
index 0000000..ff9db8b
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/StringTest.php
@@ -0,0 +1,347 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class StringInsertData {
+
+ public function __toString() {
+ return 'custom object';
+ }
+}
+
+class StringTest extends \lithium\test\Unit {
+
+ /**
+ * testUuidGeneration method
+ *
+ * @access public
+ * @return void
+ */
+ function testUuidGeneration() {
+ $this->skipIf(true);
+ $result = String::uuid();
+ $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/";
+ $match = preg_match($pattern, $result);
+ $this->assertTrue($match);
+ }
+
+ /**
+ * testMultipleUuidGeneration method
+ *
+ * @access public
+ * @return void
+ */
+ function testMultipleUuidGeneration() {
+ $this->skipIf(true);
+ $check = array();
+ $count = 700;
+ $pattern = "/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/";
+
+ for ($i = 0; $i < $count; $i++) {
+ $result = String::uuid();
+ $match = preg_match($pattern, $result);
+ $this->assertTrue($match);
+ $this->assertFalse(in_array($result, $check));
+ $check[] = $result;
+ }
+ }
+
+ /**
+ * testInsert method
+ *
+ * @access public
+ * @return void
+ */
+ function testInsert() {
+ $string = '2 + 2 = {:sum}. Lithium is {:adjective}.';
+ $expected = '2 + 2 = 4. Lithium is yummy.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'));
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = %sum. Lithium is %adjective.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'), array(
+ 'before' => '%', 'after' => ''
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = 2sum2. Lithium is 9adjective9.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'), array(
+ 'format' => '/([\d])%s\\1/'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = 12sum21. Lithium is 23adjective45.';
+ $expected = '2 + 2 = 4. Lithium is 23adjective45.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'), array(
+ 'format' => '/([\d])([\d])%s\\2\\1/'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '{:web} {:web_site}';
+ $expected = 'www http';
+ $result = String::insert($string, array('web' => 'www', 'web_site' => 'http'));
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = <sum. Lithium is <adjective>.';
+ $expected = '2 + 2 = <sum. Lithium is yummy.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'), array(
+ 'before' => '<', 'after' => '>'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = \:sum. Lithium is :adjective.';
+ $expected = '2 + 2 = :sum. Lithium is yummy.';
+ $result = String::insert(
+ $string,
+ array('sum' => '4', 'adjective' => 'yummy'),
+ array('before' => ':', 'after' => null, 'escape' => '\\')
+ );
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = !:sum. Lithium is :adjective.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'), array(
+ 'escape' => '!', 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '2 + 2 = \%sum. Lithium is %adjective.';
+ $expected = '2 + 2 = %sum. Lithium is yummy.';
+ $result = String::insert($string, array('sum' => '4', 'adjective' => 'yummy'), array(
+ 'before' => '%', 'after' => '', 'escape' => '\\'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = ':a :b \:a :a';
+ $expected = '1 2 :a 1';
+ $result = String::insert($string, array('a' => 1, 'b' => 2), array(
+ 'before' => ':', 'after' => '', 'escape' => '\\'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '{:a} {:b} {:c}';
+ $expected = '2 3';
+ $result = String::insert($string, array('b' => 2, 'c' => 3), array('clean' => true));
+ $this->assertEqual($expected, $result);
+
+ $string = '{:a} {:b} {:c}';
+ $expected = '1 3';
+ $result = String::insert($string, array('a' => 1, 'c' => 3), array('clean' => true));
+ $this->assertEqual($expected, $result);
+
+ $string = '{:a} {:b} {:c}';
+ $expected = '2 3';
+ $result = String::insert($string, array('b' => 2, 'c' => 3), array('clean' => true));
+ $this->assertEqual($expected, $result);
+
+ $string = ':a, :b and :c';
+ $expected = '2 and 3';
+ $result = String::insert($string, array('b' => 2, 'c' => 3), array(
+ 'clean' => true, 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '{:a}, {:b} and {:c}';
+ $expected = '2 and 3';
+ $result = String::insert($string, array('b' => 2, 'c' => 3), array('clean' => true));
+ $this->assertEqual($expected, $result);
+
+ $string = '"{:a}, {:b} and {:c}"';
+ $expected = '"1, 2"';
+ $result = String::insert($string, array('a' => 1, 'b' => 2), array('clean' => true));
+ $this->assertEqual($expected, $result);
+
+ $string = '"${a}, ${b} and ${c}"';
+ $expected = '"1, 2"';
+ $result = String::insert($string, array('a' => 1, 'b' => 2), array(
+ 'before' => '${', 'after' => '}', 'clean' => true
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '<img src="{:src}" alt="{:alt}" class="foo {:extra} bar"/>';
+ $expected = '<img src="foo" class="foo bar"/>';
+ $result = String::insert($string, array('src' => 'foo'), array('clean' => 'html'));
+ $this->assertEqual($expected, $result);
+
+ $string = '<img src=":src" class=":no :extra"/>';
+ $expected = '<img src="foo"/>';
+ $result = String::insert($string, array('src' => 'foo'), array(
+ 'clean' => 'html', 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual($expected, $result);
+
+ $string = '<img src="{:src}" class="{:no} {:extra}"/>';
+ $expected = '<img src="foo" class="bar"/>';
+ $result = String::insert($string, array('src' => 'foo', 'extra' => 'bar'), array(
+ 'clean' => 'html'
+ ));
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert("this is a ? string", "test");
+ $expected = "this is a test string";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert("this is a ? string with a ? ? ?", array(
+ 'long', 'few?', 'params', 'you know'
+ ));
+ $expected = "this is a long string with a few? params you know";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert(
+ 'update saved_urls set url = :url where id = :id',
+ array('url' => 'http://testurl.com/param1:url/param2:id', 'id' => 1),
+ array('before' => ':', 'after' => '')
+ );
+ $expected = "update saved_urls set url = http://testurl.com/param1:url/param2:id ";
+ $expected .= "where id = 1";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert(
+ 'update saved_urls set url = :url where id = :id',
+ array('id' => 1, 'url' => 'http://www.testurl.com/param1:url/param2:id'),
+ array('before' => ':', 'after' => '')
+ );
+ $expected = "update saved_urls set url = http://www.testurl.com/param1:url/param2:id ";
+ $expected .= "where id = 1";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert('{:me} lithium. {:subject} {:verb} fantastic.', array(
+ 'me' => 'I :verb', 'subject' => 'lithium', 'verb' => 'is'
+ ));
+ $expected = "I :verb lithium. lithium is fantastic.";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert(':I.am: :not.yet: passing.', array('I.am' => 'We are'), array(
+ 'before' => ':', 'after' => ':', 'clean' => array(
+ 'replacement' => ' of course', 'method' => 'text'
+ )
+ ));
+ $expected = "We are of course passing.";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert(
+ ':I.am: :not.yet: passing.',
+ array('I.am' => 'We are'),
+ array('before' => ':', 'after' => ':', 'clean' => true)
+ );
+ $expected = "We are passing.";
+ $this->assertEqual($expected, $result);
+
+ $result = String::insert('?-pended result', array('Pre'));
+ $expected = "Pre-pended result";
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests casting/inserting of custom objects with `String::insert()`.
+ *
+ * @return void
+ */
+ public function testInsertWithObject() {
+ $foo = new StringInsertData();
+ $result = String::insert('This is a {:foo}', compact('foo'));
+ $expected = 'This is a custom object';
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * test Clean Insert
+ *
+ * @return void
+ **/
+ function testCleanInsert() {
+ $result = String::clean(':incomplete', array(
+ 'clean' => true, 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual('', $result);
+
+ $result = String::clean(':incomplete', array(
+ 'clean' => array('method' => 'text', 'replacement' => 'complete'),
+ 'before' => ':', 'after' => '')
+ );
+ $this->assertEqual('complete', $result);
+
+ $result = String::clean(':in.complete', array(
+ 'clean' => true, 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual('', $result);
+
+ $result = String::clean(':in.complete and', array(
+ 'clean' => true, 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual('', $result);
+
+ $result = String::clean(':in.complete or stuff', array(
+ 'clean' => true, 'before' => ':', 'after' => ''
+ ));
+ $this->assertEqual('stuff', $result);
+
+ $result = String::clean(
+ '<p class=":missing" id=":missing">Text here</p>',
+ array('clean' => 'html', 'before' => ':', 'after' => '')
+ );
+ $this->assertEqual('<p>Text here</p>', $result);
+
+ $string = ':a 2 3';
+ $result = String::clean($string, array('clean' => true, 'before' => ':', 'after' => ''));
+ $this->assertEqual('2 3', $result);
+
+ $result = String::clean($string, array('clean' => false, 'before' => ':', 'after' => ''));
+ $this->assertEqual($string, $result);
+ }
+
+ /**
+ * testTokenize method
+ *
+ * @access public
+ * @return void
+ */
+ function testTokenize() {
+ $result = String::tokenize('A,(short,boring test)');
+ $expected = array('A', '(short,boring test)');
+ $this->assertEqual($expected, $result);
+
+ $result = String::tokenize('A,(short,more interesting( test)');
+ $expected = array('A', '(short,more interesting( test)');
+ $this->assertEqual($expected, $result);
+
+ $result = String::tokenize('A,(short,very interesting( test))');
+ $expected = array('A', '(short,very interesting( test))');
+ $this->assertEqual($expected, $result);
+
+ $result = String::tokenize('"single tag"', ' ', '"', '"');
+ $expected = array('"single tag"');
+ $this->assertEqual($expected, $result);
+
+ $result = String::tokenize('tagA "single tag" tagB', ' ', '"', '"');
+ $expected = array('tagA', '"single tag"', 'tagB');
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests the `String::extract()` regex helper method.
+ *
+ * @return void
+ */
+ public function testStringExtraction() {
+ $result = String::extract('/string/', 'whole string');
+ $this->assertEqual('string', $result);
+
+ $this->assertFalse(String::extract('/not/', 'whole string'));
+ $this->assertEqual('part', String::extract('/\w+\s*(\w+)/', 'second part', 1));
+ $this->assertNull(String::extract('/\w+\s*(\w+)/', 'second part', 2));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/ValidatorTest.php b/libraries/lithium/tests/cases/util/ValidatorTest.php
new file mode 100644
index 0000000..6ce3789
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/ValidatorTest.php
@@ -0,0 +1,840 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class ValidatorTest extends \lithium\test\Unit {
+
+ function setUp() {
+ Validator::__init();
+ }
+
+ /**
+ * Tests static method call routing to enable patterns defined in Validator::$_rules to be
+ * called as methods.
+ *
+ * @return void
+ */
+ public function testCustomMethodDispatching() {
+ $this->assertTrue(Validator::isRegex('/^abc$/'));
+ $this->assertTrue(Validator::isPhone('800-999-5555'));
+
+ $this->assertTrue(Validator::isUrl('http://google.com'));
+ $this->assertTrue(Validator::isUrl('google.com', 'loose'));
+ $this->assertTrue(Validator::isUrl('google.com'));
+ $this->assertFalse(Validator::isUrl('google.com', 'strict'));
+
+ $this->assertTrue(Validator::isSsn('478364120'));
+ $this->assertTrue(Validator::isSsn('478-36-4120'));
+ $this->assertFalse(Validator::isSsn('478-36-41200'));
+ }
+
+ /**
+ * Tests that new methods can be called on Validator by adding rules using Validator::add().
+ *
+ * @return void
+ */
+ public function testAddCustomRegexMethods() {
+ // $this->expectException("Rule 'foo' is not a validation rule");
+ // $this->assertNull(Validator::isFoo('foo'));
+
+ Validator::add('foo', '/^foo$/');
+ $this->assertTrue(Validator::isFoo('foo'));
+ $this->assertFalse(Validator::isFoo('bar'));
+
+ // $this->expectException("Rule 'uuid' is not a validation rule");
+ // $this->assertNull(Validator::isUuid('1c0a5830-6025-11de-8a39-0800200c9a66'));
+
+ $uuid = '/[A-Fa-f0-9]{8}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{4}-[A-Fa-f0-9]{12}/';
+ Validator::add('uuid', $uuid);
+ $this->assertTrue(Validator::isUuid('1c0a5830-6025-11de-8a39-0800200c9a66'));
+ $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'));
+ }
+
+ /**
+ * Tests that the rules state is reset when calling `Validator::__init()`.
+ *
+ * @return void
+ */
+ public function testStateReset() {
+ Validator::__init();
+ }
+
+ /**
+ * Tests that new formats can be added to existing regex methods using Validator::add().
+ *
+ * @return void
+ */
+ public function testAddCustomRegexFormats() {
+ $this->assertTrue(Validator::isPostalCode('11201'));
+ $this->assertTrue(Validator::isPostalCode('11201-0456'));
+
+ $this->assertTrue(Validator::isPostalCode('11201', 'us'));
+ $this->assertTrue(Validator::isPostalCode('11201-0456', 'us'));
+
+ $this->assertFalse(Validator::isPostalCode('foo-bar'));
+ Validator::add(array('postalCode' => array('foo' => '/^foo-bar$/')));
+
+ $this->assertTrue(Validator::isPostalCode('foo-bar'));
+ $this->assertTrue(Validator::isPostalCode('foo-bar', 'foo'));
+ $this->assertTrue(Validator::isPostalCode('foo-bar', 'any'));
+ $this->assertFalse(Validator::isPostalCode('foo-bar', 'us'));
+ }
+
+ public function testPrefilterMethodAccess() {
+ $this->assertTrue(Validator::isNotEmpty('0'));
+ $this->assertFalse(Validator::isNotEmpty(''));
+ $this->assertFalse(Validator::isNotEmpty(null));
+ }
+
+ /**
+ * Tests that the 'notEmpty' rule validates correct values
+ *
+ * @return void
+ */
+ public function testNotEmptyRule() {
+ $this->assertTrue(Validator::isNotEmpty('abcdefg'));
+ $this->assertTrue(Validator::isNotEmpty('fasdf '));
+ $this->assertTrue(Validator::isNotEmpty('fooo'.chr(243).'blabla'));
+ $this->assertTrue(Validator::isNotEmpty('abçďĕʑʘπй'));
+ $this->assertTrue(Validator::isNotEmpty('José'));
+ $this->assertTrue(Validator::isNotEmpty('é'));
+ $this->assertTrue(Validator::isNotEmpty('π'));
+ $this->assertFalse(Validator::isNotEmpty("\t "));
+ $this->assertFalse(Validator::isNotEmpty(""));
+ }
+
+ /**
+ * Tests the the 'alphaNumeric' rule validates correct values.
+ *
+ * @return void
+ */
+ public function testAlphaNumeric() {
+ $this->assertTrue(Validator::isAlphaNumeric('frferrf'));
+ $this->assertTrue(Validator::isAlphaNumeric('12234'));
+ $this->assertTrue(Validator::isAlphaNumeric('1w2e2r3t4y'));
+ $this->assertTrue(Validator::isAlphaNumeric('0'));
+ $this->assertTrue(Validator::isAlphaNumeric('abçďĕʑʘπй'));
+ $this->assertTrue(Validator::isAlphaNumeric('ˇˆๆゞ'));
+ $this->assertTrue(Validator::isAlphaNumeric('אกあアꀀ豈'));
+ $this->assertTrue(Validator::isAlphaNumeric('Džᾈᾨ'));
+ $this->assertTrue(Validator::isAlphaNumeric('ÆΔΩЖÇ'));
+
+
+ $this->assertFalse(Validator::isAlphaNumeric('12 234'));
+ $this->assertFalse(Validator::isAlphaNumeric('dfd 234'));
+ $this->assertFalse(Validator::isAlphaNumeric("\n"));
+ $this->assertFalse(Validator::isAlphaNumeric("\t"));
+ $this->assertFalse(Validator::isAlphaNumeric("\r"));
+ $this->assertFalse(Validator::isAlphaNumeric(' '));
+ $this->assertFalse(Validator::isAlphaNumeric(''));
+ }
+
+ /**
+ * Tests the the 'lengthBetween' rule validates correct values.
+ *
+ * @return void
+ */
+ public function testIsLengthBetweenRule() {
+ $this->assertTrue(Validator::isLengthBetween('abcde', null, array('min' => 1, 'max' => 7)));
+ $this->assertTrue(Validator::isLengthBetween('', null, array('min' => 0, 'max' => 7)));
+ $this->assertFalse(Validator::isLengthBetween('abcd', null, array('min' => 1, 'max' => 3)));
+ }
+
+ public function testIsNumericRule() {
+ $this->assertTrue(Validator::isNumeric(0));
+ $this->assertTrue(Validator::isNumeric('0'));
+ $this->assertTrue(Validator::isNumeric('-0'));
+ $this->assertFalse(Validator::isNumeric('-'));
+ }
+
+ public function testBooleanValidation() {
+ $this->assertTrue(Validator::isBoolean(true));
+ $this->assertTrue(Validator::isBoolean(false));
+ $this->assertTrue(Validator::isBoolean(0));
+ $this->assertTrue(Validator::isBoolean(1));
+ $this->assertTrue(Validator::isBoolean('0'));
+ $this->assertTrue(Validator::isBoolean('1'));
+
+ $this->assertFalse(Validator::isBoolean('11'));
+ $this->assertFalse(Validator::isBoolean('-1'));
+ $this->assertFalse(Validator::isBoolean(-1));
+ $this->assertFalse(Validator::isBoolean(11));
+ $this->assertFalse(Validator::isBoolean(null));
+ }
+
+ /**
+ * Test basic decimal number validation.
+ *
+ * @return void
+ */
+ function testDecimal() {
+ $this->assertTrue(Validator::isDecimal('+1234.54321'));
+ $this->assertTrue(Validator::isDecimal('-1234.54321'));
+ $this->assertTrue(Validator::isDecimal('1234.54321'));
+ $this->assertTrue(Validator::isDecimal('+0123.45e6'));
+ $this->assertTrue(Validator::isDecimal('-0123.45e6'));
+ $this->assertTrue(Validator::isDecimal('0123.45e6'));
+ $this->assertFalse(Validator::isDecimal('string'));
+ $this->assertFalse(Validator::isDecimal('1234'));
+ $this->assertFalse(Validator::isDecimal('-1234'));
+ $this->assertFalse(Validator::isDecimal('+1234'));
+ }
+
+ /**
+ * Test decimal validation with precision specified.
+ *
+ * @access public
+ * @return void
+ */
+ public function testDecimalWithPlaces() {
+ $this->assertTrue(Validator::isDecimal('.27', null, array('precision' => '2')));
+ $this->assertTrue(Validator::isDecimal(.27, null, array('precision' => 2)));
+ $this->assertTrue(Validator::isDecimal(-.27, null, array('precision' => 2)));
+ $this->assertTrue(Validator::isDecimal(+.27, null, array('precision' => 2)));
+ $this->assertTrue(Validator::isDecimal('.277', null, array('precision' => '3')));
+ $this->assertTrue(Validator::isDecimal(.277, null, array('precision' => 3)));
+ $this->assertTrue(Validator::isDecimal(-.277, null, array('precision' => 3)));
+ $this->assertTrue(Validator::isDecimal(+.277, null, array('precision' => 3)));
+ $this->assertTrue(Validator::isDecimal('1234.5678', null, array('precision' => '4')));
+ $this->assertTrue(Validator::isDecimal(1234.5678, null, array('precision' => 4)));
+ $this->assertTrue(Validator::isDecimal(-1234.5678, null, array('precision' => 4)));
+ $this->assertTrue(Validator::isDecimal(+1234.5678, null, array('precision' => 4)));
+ $this->assertFalse(Validator::isDecimal('1234.5678', null, array('precision' => '3')));
+ $this->assertFalse(Validator::isDecimal(1234.5678, null, array('precision' => 3)));
+ $this->assertFalse(Validator::isDecimal(-1234.5678, null, array('precision' => 3)));
+ $this->assertFalse(Validator::isDecimal(+1234.5678, null, array('precision' => 3)));
+ }
+
+ public function testEmailValidation() {
+ $this->assertTrue(Validator::isEmail('abc.efg@domain.com'));
+ $this->assertTrue(Validator::isEmail('efg@domain.com'));
+ $this->assertTrue(Validator::isEmail('abc-efg@domain.com'));
+ $this->assertTrue(Validator::isEmail('abc_efg@domain.com'));
+ $this->assertTrue(Validator::isEmail('raw@test.ra.ru'));
+ $this->assertTrue(Validator::isEmail('abc-efg@domain-hyphened.com'));
+ $this->assertTrue(Validator::isEmail("p.o'malley@domain.com"));
+ $this->assertTrue(Validator::isEmail('abc+efg@domain.com'));
+ $this->assertTrue(Validator::isEmail('abc&efg@domain.com'));
+ $this->assertTrue(Validator::isEmail('abc.efg@12345.com'));
+ $this->assertTrue(Validator::isEmail('abc.efg@12345.co.jp'));
+ $this->assertTrue(Validator::isEmail('abc@g.cn'));
+ $this->assertTrue(Validator::isEmail('abc@x.com'));
+ $this->assertTrue(Validator::isEmail('henrik@sbcglobal.net'));
+ $this->assertTrue(Validator::isEmail('sani@sbcglobal.net'));
+
+ /**
+ * All ICANN TLDs
+ */
+ $this->assertTrue(Validator::isEmail('abc@example.aero'));
+ $this->assertTrue(Validator::isEmail('abc@example.asia'));
+ $this->assertTrue(Validator::isEmail('abc@example.biz'));
+ $this->assertTrue(Validator::isEmail('abc@example.cat'));
+ $this->assertTrue(Validator::isEmail('abc@example.com'));
+ $this->assertTrue(Validator::isEmail('abc@example.coop'));
+ $this->assertTrue(Validator::isEmail('abc@example.edu'));
+ $this->assertTrue(Validator::isEmail('abc@example.gov'));
+ $this->assertTrue(Validator::isEmail('abc@example.info'));
+ $this->assertTrue(Validator::isEmail('abc@example.int'));
+ $this->assertTrue(Validator::isEmail('abc@example.jobs'));
+ $this->assertTrue(Validator::isEmail('abc@example.mil'));
+ $this->assertTrue(Validator::isEmail('abc@example.mobi'));
+ $this->assertTrue(Validator::isEmail('abc@example.museum'));
+ $this->assertTrue(Validator::isEmail('abc@example.name'));
+ $this->assertTrue(Validator::isEmail('abc@example.net'));
+ $this->assertTrue(Validator::isEmail('abc@example.org'));
+ $this->assertTrue(Validator::isEmail('abc@example.pro'));
+ $this->assertTrue(Validator::isEmail('abc@example.tel'));
+ $this->assertTrue(Validator::isEmail('abc@example.travel'));
+ $this->assertTrue(Validator::isEmail('someone@st.t-com.hr'));
+
+ /**
+ * Strange, but technically valid email addresses
+ */
+ $email = 'S=postmaster/OU=rz/P=uni-frankfurt/A=d400/C=de@gateway.d400.de';
+ $this->assertTrue(Validator::isEmail($email));
+ $this->assertTrue(Validator::isEmail('customer/department=shipping@example.com'));
+ $this->assertTrue(Validator::isEmail('$A12345@example.com'));
+ $this->assertTrue(Validator::isEmail('!def!xyz%abc@example.com'));
+ $this->assertTrue(Validator::isEmail('_somename@example.com'));
+
+ /**
+ * Invalid addresses
+ */
+ $this->assertFalse(Validator::isEmail('abc@example'));
+ $this->assertFalse(Validator::isEmail('abc@example.c'));
+ $this->assertFalse(Validator::isEmail('abc@example.com.'));
+ $this->assertFalse(Validator::isEmail('abc.@example.com'));
+ $this->assertFalse(Validator::isEmail('abc@example..com'));
+ $this->assertFalse(Validator::isEmail('abc@example.com.a'));
+ $this->assertFalse(Validator::isEmail('abc@example.toolong'));
+ $this->assertFalse(Validator::isEmail('abc;@example.com'));
+ $this->assertFalse(Validator::isEmail('abc@example.com;'));
+ $this->assertFalse(Validator::isEmail('abc@efg@example.com'));
+ $this->assertFalse(Validator::isEmail('abc@@example.com'));
+ $this->assertFalse(Validator::isEmail('abc efg@example.com'));
+ $this->assertFalse(Validator::isEmail('abc,efg@example.com'));
+ $this->assertFalse(Validator::isEmail('abc@sub,example.com'));
+ $this->assertFalse(Validator::isEmail("abc@sub'example.com"));
+ $this->assertFalse(Validator::isEmail('abc@sub/example.com'));
+ $this->assertFalse(Validator::isEmail('abc@yahoo!.com'));
+ $this->assertFalse(Validator::isEmail("Nyrée.surname@example.com"));
+ $this->assertFalse(Validator::isEmail('abc@example_underscored.com'));
+ $this->assertFalse(Validator::isEmail('raw@test.ra.ru....com'));
+ }
+
+ /**
+ * Tests email address validation, with additional hostname lookup
+ *
+ * @return void
+ */
+ public function testEmailDomainCheck() {
+ $this->assertTrue(Validator::isEmail('abc.efg@lithiumphp.org', null, array('deep' => true)));
+ $this->assertFalse(Validator::isEmail('abc.efg@caphpkeinvalid.com', null, array(
+ 'deep' => true
+ )));
+ $this->assertFalse(Validator::isEmail('abc@example.abcd', null, array('deep' => true)));
+ }
+
+ /**
+ * Tests 'inList' validation.
+ *
+ * @return void
+ */
+ function testInList() {
+ $this->assertTrue(Validator::isInList('one', null, array('list' => array('one', 'two'))));
+ $this->assertTrue(Validator::isInList('two', null, array('list' => array('one', 'two'))));
+ $this->assertFalse(Validator::isInList('3', null, array('list' => array('one', 'two'))));
+ }
+
+
+ /**
+ * Tests credit card validation for numbers in various vendors' formats.
+ *
+ * @return void
+ */
+ public function testCreditCardValidation() {
+
+ /**
+ * American Express
+ */
+ $this->assertTrue(Validator::isCreditCard('370482756063980', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('3491-0643-3773-483', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('344671486204764', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('344042544509943', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('377147515754475', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('375239372816422', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('376294341957707', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('341779292230411', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('341646919853372', 'amex'));
+ $this->assertTrue(Validator::isCreditCard('348498616319346', 'amex', array(
+ 'deep' => true
+ )));
+
+ /**
+ * BankCard
+ */
+ $this->assertTrue(Validator::isCreditCard('5610 7458 6741 3420', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5610376649499352', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5610091936000694', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5602248780118788', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5610631567676765', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5602238211270795', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5610173951215470', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5610139705753702', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5602226032150551', 'bankcard'));
+ $this->assertTrue(Validator::isCreditCard('5602223993735777', 'bankcard'));
+
+ /**
+ * Diners Club 14
+ */
+ $this->assertTrue(Validator::isCreditCard('30155483651028', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36371312803821', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('38801277489875', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30348560464296', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30349040317708', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36567413559978', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36051554732702', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30391842198191', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30172682197745', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30162056566641', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30085066927745', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36519025221976', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30372679371044', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('38913939150124', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36852899094637', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30138041971120', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36184047836838', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30057460264462', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('38980165212050', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30356516881240', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('38744810033182', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30173638706621', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30158334709185', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30195413721186', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('38863347694793', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30275627009113', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30242860404971', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('30081877595151', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('38053196067461', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36520379984870', 'diners'));
+
+ /**
+ * 2004 MasterCard/Diners Club Alliance International 14
+ */
+ $this->assertTrue(Validator::isCreditCard('36747701998969', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36427861123159', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36150537602386', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36582388820610', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('36729045250216', 'diners'));
+
+ /**
+ * 2004 MasterCard/Diners Club Alliance US & Canada 16
+ */
+ $this->assertTrue(Validator::isCreditCard('5597511346169950', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('5526443162217562', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('5577265786122391', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('5534061404676989', 'diners'));
+ $this->assertTrue(Validator::isCreditCard('5545313588374502', 'diners'));
+
+ /**
+ * Discover
+ */
+ $this->assertTrue(Validator::isCreditCard('6011802876467237', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6506432777720955', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6011126265283942', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6502187151579252', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6506600836002298', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6504376463615189', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6011440907005377', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6509735979634270', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6011422366775856', 'disc'));
+ $this->assertTrue(Validator::isCreditCard('6500976374623323', 'disc'));
+
+ /**
+ * enRoute
+ */
+ $this->assertTrue(Validator::isCreditCard('201496944158937', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('214945833739665', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('214982692491187', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('214901395949424', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('201480676269187', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('214911922887807', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('201485025457250', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('201402662758866', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('214981579370225', 'enroute'));
+ $this->assertTrue(Validator::isCreditCard('201447595859877', 'enroute'));
+
+ /**
+ * JCB 15 digit
+ */
+ $this->assertTrue(Validator::isCreditCard('210034762247893', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('180078671678892', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('180010559353736', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('210095474464258', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('210006675562188', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('210063299662662', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('180032506857825', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('210057919192738', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('180031358949367', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('180033802147846', 'jcb'));
+
+ /**
+ * JCB 16 digit
+ */
+ $this->assertTrue(Validator::isCreditCard('3096806857839939', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3158699503187091', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3112549607186579', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3112332922425604', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3112001541159239', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3112162495317841', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3337562627732768', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3337107161330775', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528053736003621', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528915255020360', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3096786059660921', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528264799292320', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3096469164130136', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3112127443822853', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3096849995802328', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528090735127407', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3112101006819234', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3337444428040784', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3088043154151061', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3088295969414866', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3158748843158575', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3158709206148538', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3158365159575324', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3158671691305165', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528523028771093', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3096057126267870', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3158514047166834', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528274546125962', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3528890967705733', 'jcb'));
+ $this->assertTrue(Validator::isCreditCard('3337198811307545', 'jcb'));
+
+ /**
+ * Maestro (debit card)
+ */
+ $this->assertTrue(Validator::isCreditCard('5020147409985219', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020931809905616', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020412965470224', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020129740944022', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020024696747943', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020581514636509', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020695008411987', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('5020565359718977', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('6339931536544062', 'maestro'));
+ $this->assertTrue(Validator::isCreditCard('6465028615704406', 'maestro'));
+
+ /**
+ * MasterCard
+ */
+ $this->assertTrue(Validator::isCreditCard('5580424361774366', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5589563059318282', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5387558333690047', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5163919215247175', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5386742685055055', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5102303335960674', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5526543403964565', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5538725892618432', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5119543573129778', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5391174753915767', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5510994113980714', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5183720260418091', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5488082196086704', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5484645164161834', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5171254350337031', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5526987528136452', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5504148941409358', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5240793507243615', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5162114693017107', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5163104807404753', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5590136167248365', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5565816281038948', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5467639122779531', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5297350261550024', 'mc'));
+ $this->assertTrue(Validator::isCreditCard('5162739131368058', 'mc'));
+
+ /**
+ * Solo 16
+ */
+ $this->assertTrue(Validator::isCreditCard('6767432107064987', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334667758225411', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767037421954068', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767823306394854', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334768185398134', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767286729498589', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334972104431261', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334843427400616', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767493947881311', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767194235798817', 'solo'));
+
+ /**
+ * Solo 18
+ */
+ $this->assertTrue(Validator::isCreditCard('676714834398858593', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('676751666435130857', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('676781908573924236', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('633488724644003240', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('676732252338067316', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('676747520084495821', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('633465488901381957', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('633487484858610484', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('633453764680740694', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('676768613295414451', 'solo'));
+
+ /**
+ * Solo 19
+ */
+ $this->assertTrue(Validator::isCreditCard('6767838565218340113', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767760119829705181', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767265917091593668', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767938856947440111', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6767501945697390076', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334902868716257379', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334922127686425532', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334933119080706440', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334647959628261714', 'solo'));
+ $this->assertTrue(Validator::isCreditCard('6334527312384101382', 'solo'));
+
+ /**
+ * Switch 16
+ */
+ $this->assertTrue(Validator::isCreditCard('5641829171515733', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641824852820809', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759129648956909', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759626072268156', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641822698388957', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641827123105470', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641823755819553', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641821939587682', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936097148079186', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641829739125009', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641822860725507', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936717688865831', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759487613615441', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641821346840617', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641825793417126', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641821302759595', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759784969918837', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641824910667036', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759139909636173', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333425070638022', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641823910382067', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936295218139423', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333031811316199', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936912044763198', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936387053303824', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759535838760523', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333427174594051', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641829037102700', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641826495463046', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333480852979946', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641827761302876', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641825083505317', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759298096003991', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936119165483420', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936190990500993', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4903356467384927', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333372765092554', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641821330950570', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759841558826118', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936164540922452', 'switch'));
+
+ /**
+ * Switch 18
+ */
+ $this->assertTrue(Validator::isCreditCard('493622764224625174', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182823396913535', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675917308304801234', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675919890024220298', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633308376862556751', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182377633208779', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182870014926787', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675979788553829819', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493668394358130935', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493637431790930965', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633321438601941513', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675913800898840986', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182592016841547', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182428380440899', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493696376827623463', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675977939286485757', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('490302699502091579', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182085013662230', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493693054263310167', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633321755966697525', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675996851719732811', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493699211208281028', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493697817378356614', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675968224161768150', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493669416873337627', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182439172549714', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675926914467673598', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182565231977809', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675966282607849002', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493691609704348548', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675933118546065120', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493631116677238592', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675921142812825938', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633338311815675113', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633323539867338621', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('675964912740845663', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633334008833727504', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('493631941273687169', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('564182971729706785', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('633303461188963496', 'switch'));
+
+ /**
+ * Switch 19
+ */
+ $this->assertTrue(Validator::isCreditCard('6759603460617628716', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936705825268647681', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641829846600479183', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759389846573792530', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936189558712637603', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641822217393868189', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4903075563780057152', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936510653566569547', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936503083627303364', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936777334398116272', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641823876900554860', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759619236903407276', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759011470269978117', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333175833997062502', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759498728789080439', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4903020404168157841', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759354334874804313', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759900856420875115', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641827269346868860', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641828995047453870', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333321884754806543', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333108246283715901', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759572372800700102', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4903095096797974933', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333354315797920215', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759163746089433755', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759871666634807647', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641827883728575248', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936527975051407847', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641823318396882141', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759123772311123708', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4903054736148271088', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936477526808883952', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936433964890967966', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6333245128906049344', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936321036970553134', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936111816358702773', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('4936196077254804290', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('6759558831206830183', 'switch'));
+ $this->assertTrue(Validator::isCreditCard('5641827998830403137', 'switch'));
+
+ /**
+ * Visa 13 digit
+ */
+ $this->assertTrue(Validator::isCreditCard('4024007174754', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4104816460717', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716229700437', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539305400213', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4728260558665', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929100131792', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007117308', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539915491024', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539790901139', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485284914909', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4782793022350', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4556899290685', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007134774', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4333412341316', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539534204543', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485640373626', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929911445746', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539292550806', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716523014030', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007125152', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539758883311', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007103258', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916933155767', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007159672', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716935544871', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929415177779', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929748547896', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929153468612', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539397132104', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485293435540', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485799412720', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916744757686', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4556475655426', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539400441625', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485437129173', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716253605320', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539366156589', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916498061392', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716127163779', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007183078', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4041553279654', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532380121960', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485906062491', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539365115149', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485146516702', 'visa'));
+
+ /**
+ * Visa 16 digit
+ */
+ $this->assertTrue(Validator::isCreditCard('4916375389940009', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929167481032610', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485029969061519', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485573845281759', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485669810383529', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929615806560327', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4556807505609535', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532611336232890', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532201952422387', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485073797976290', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007157580969', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4053740470212274', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716265831525676', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007100222966', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539556148303244', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532449879689709', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916805467840986', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532155644440233', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4467977802223781', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539224637000686', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4556629187064965', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532970205932943', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4821470132041850', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916214267894485', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4024007169073284', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716783351296122', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4556480171913795', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929678411034997', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4682061913519392', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916495481746474', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929007108460499', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539951357838586', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716482691051558', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916385069917516', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929020289494641', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532176245263774', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4556242273553949', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4481007485188614', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4716533372139623', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4929152038152632', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4539404037310550', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4532800925229140', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4916845885268360', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4394514669078434', 'visa'));
+ $this->assertTrue(Validator::isCreditCard('4485611378115042', 'visa'));
+
+ /**
+ * Visa Electron
+ */
+ $this->assertTrue(Validator::isCreditCard('4175003346287100', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4913042516577228', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4917592325659381', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4917084924450511', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4917994610643999', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4175005933743585', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4175008373425044', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4913119763664154', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4913189017481812', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4913085104968622', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4175008803122021', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4913294453962489', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4175009797419290', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4175005028142917', 'electron'));
+ $this->assertTrue(Validator::isCreditCard('4913940802385364', 'electron'));
+
+ /**
+ * Voyager
+ */
+ $this->assertTrue(Validator::isCreditCard('869940697287073', 'voyager'));
+ $this->assertTrue(Validator::isCreditCard('869934523596112', 'voyager'));
+ $this->assertTrue(Validator::isCreditCard('869958670174621', 'voyager'));
+ $this->assertTrue(Validator::isCreditCard('869921250068209', 'voyager'));
+ $this->assertTrue(Validator::isCreditCard('869972521242198', 'voyager'));
+
+ $this->assertTrue(Validator::isLuhn('869972521242198'));
+ $this->assertFalse(Validator::isLuhn(false));
+ $this->assertFalse(Validator::isLuhn(null));
+ $this->assertFalse(Validator::isLuhn(''));
+ $this->assertFalse(Validator::isLuhn(true));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/audit/LoggerTest.php b/libraries/lithium/tests/cases/util/audit/LoggerTest.php
new file mode 100644
index 0000000..4a9c156
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/audit/LoggerTest.php
@@ -0,0 +1,87 @@
+<?php
+
+namespace lithium\tests\cases\util\audit;
+
+use \lithium\util\audit\Logger;
+use \lithium\util\Collection;
+
+/**
+ * Mock class used for testing Logger adapter
+ *
+ */
+class Test extends \lithium\core\Object {
+
+ public function write($name, $value) {
+ return function($self, $params, $chain) {
+ return true;
+ };
+ }
+}
+
+/**
+ * Logger adapter test case
+ *
+ */
+class LoggerTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ Logger::config(array('default' => array('adapter' => new Test())));
+ }
+
+ public function tearDown() {
+ Logger::reset();
+ }
+
+ public function testConfig() {
+ $test = new Test();
+ $config = array('logger' => array('adapter' => $test, 'filters' => array()));
+
+ $result = Logger::config($config);
+ $expected = new Collection(array('items' => $config));
+
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testReset() {
+ $test = new Test();
+ $config = array('logger' => array('adapter' => $test, 'filters' => array()));
+
+ $result = Logger::config($config);
+
+ $result = Logger::reset();
+ $this->assertNull($result);
+
+ $result = Logger::config();
+ $this->assertEqual(new Collection(), $result);
+ }
+
+ public function testWrite() {
+ $result = Logger::write('default', 'value');
+ $this->assertTrue($result);
+ }
+
+ public function testIntegrationWriteFile() {
+ $config = array('default' => array('adapter' => 'File'));
+ Logger::config($config);
+
+ $result = Logger::write('default', 'Message line 1');
+ $this->assertTrue(file_exists(LITHIUM_APP_PATH . '/tmp/logs/default.log'));
+
+ $expected = "Message line 1\n";
+ $result = file_get_contents(LITHIUM_APP_PATH . '/tmp/logs/default.log');
+ $this->assertEqual($expected, $result);
+
+ $result = Logger::write('default', 'Message line 2');
+ $this->assertTrue($result);
+
+ $expected = "Message line 1\nMessage line 2\n";
+ $result = file_get_contents(LITHIUM_APP_PATH . '/tmp/logs/default.log');
+ $this->assertEqual($expected, $result);
+
+ unlink(LITHIUM_APP_PATH . '/tmp/logs/default.log');
+ }
+
+}
+
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/audit/adapters/FileTest.php b/libraries/lithium/tests/cases/util/audit/adapters/FileTest.php
new file mode 100644
index 0000000..7139574
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/audit/adapters/FileTest.php
@@ -0,0 +1,16 @@
+<?php
+
+namespace lithium\tests\cases\util\audit\adapters;
+
+use \lithium\audit\logger\adapters\File;
+
+class FileTest extends \lithium\test\Unit {
+
+ public function setUp() {
+ $this->path = LITHIUM_APP_PATH . '/tmp/logs/';
+ $this->Adapter = new File(array('path' => $this->path));
+ }
+
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/reflection/InspectorTest.php b/libraries/lithium/tests/cases/util/reflection/InspectorTest.php
new file mode 100644
index 0000000..a958db4
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/reflection/InspectorTest.php
@@ -0,0 +1,182 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\util\reflection;
+
+use \ReflectionClass;
+use \ReflectionMethod;
+use \lithium\util\reflection\Inspector;
+use \lithium\core\Libraries;
+
+class InspectorTest extends \lithium\test\Unit {
+
+ /**
+ * Tests that basic method lists and information are queried properly.
+ *
+ * @return void
+ */
+ public function testBasicMethodInspection() {
+ $class = '\lithium\util\reflection\Inspector';
+ $parent = '\lithium\core\StaticObject';
+
+ $expected = array_diff(get_class_methods($class), get_class_methods($parent));
+ $result = array_keys(Inspector::methods($class, 'extents'));
+ $this->assertEqual(array_intersect($result, $expected), $result);
+
+ $result = array_keys(Inspector::methods($class, 'extents', array(
+ 'self' => true, 'public' => true
+ )));
+ $this->assertEqual($expected, $result);
+
+ $result = Inspector::methods($class, 'ranges');
+ }
+
+ public function testMethodInspection() {
+ $result = Inspector::methods($this, null);
+ $this->assertTrue($result[0] instanceof ReflectionMethod);
+ }
+
+ /**
+ * Tests that the range of executable lines of this test method is properly calculated.
+ * Recursively meta.
+ *
+ * @return void
+ */
+ public function testMethodRange() {
+ $result = Inspector::methods(__CLASS__, 'ranges', array('methods' => __FUNCTION__));
+ $expected = array(__FUNCTION__ => array(__LINE__ - 1, __LINE__, __LINE__ + 1));
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Gets the executable line numbers of this file based on a manual entry of line ranges. Will
+ * need to be updated manually if this method changes.
+ *
+ * @return void
+ */
+ public function testExecutableLines() {
+ do {
+ // These lines should be ignored
+ } while (false);
+
+ $result = Inspector::executable($this, array('methods' => __FUNCTION__));
+ $expected = array(__LINE__ - 1, __LINE__, __LINE__ + 1);
+ $this->assertEqual($expected, $result);
+ }
+
+ /**
+ * Tests reading specific line numbers of a file.
+ *
+ * @return void
+ */
+ public function testLineIntrospection() {
+ $result = Inspector::lines(__FILE__, array(__LINE__ - 1));
+ $expected = array(__LINE__ - 2 => "\tpublic function testLineIntrospection() {");
+ $this->assertEqual($expected, $result);
+
+ $result = Inspector::lines(__CLASS__, array(20));
+ $expected = array(20 => 'class InspectorTest extends \lithium\test\Unit {');
+ $this->assertEqual($expected, $result);
+
+ $this->expectException('/Missing argument 2/');
+ $this->assertNull(Inspector::lines('\lithium\core\Foo'));
+ $this->assertNull(Inspector::lines(__CLASS__, array()));
+ }
+
+ /**
+ * Tests getting a list of parent classes from an object or string class name.
+ *
+ * @return void
+ */
+ public function testClassParents() {
+ $result = Inspector::parents($this);
+ $this->assertEqual('lithium\test\Unit', current($result));
+
+ $result2 = Inspector::parents(__CLASS__);
+ $this->assertEqual($result2, $result);
+
+ $this->assertFalse(Inspector::parents('lithium\core\Foo', array('autoLoad' => false)));
+ }
+
+ public function testClassFileIntrospection() {
+ $result = Inspector::classes(array('file' => __FILE__));
+ $this->assertEqual(array(__CLASS__ => __FILE__), $result);
+
+ $result = Inspector::classes(array('file' => __FILE__, 'group' => 'files'));
+ $this->assertEqual(1, count($result));
+ $this->assertEqual(__FILE__, key($result));
+
+ $result = Inspector::classes(array('file' => __FILE__, 'group' => 'foo'));
+ $this->assertEqual(array(), $result);
+ }
+
+ /**
+ * Tests that names of classes, methods, properties and namespaces are parsed properly from
+ * strings.
+ *
+ * @return void
+ */
+ public function testTypeDetection() {
+ $this->assertEqual('namespace', Inspector::type('\lithium\util'));
+ $this->assertEqual('namespace', Inspector::type('\lithium\util\reflection'));
+ $this->assertEqual('class', Inspector::type('\lithium\util\reflection\Inspector'));
+ $this->assertEqual('property', Inspector::type('Inspector::$_classes'));
+ $this->assertEqual('method', Inspector::type('Inspector::type'));
+ $this->assertEqual('method', Inspector::type('Inspector::type()'));
+ }
+
+ /**
+ * Tests getting reflection information based on a string identifier.
+ *
+ * @return void
+ */
+ public function testIdentifierIntrospection() {
+ $result = Inspector::info(__METHOD__);
+ $this->assertEqual(array('public'), $result['modifiers']);
+ $this->assertEqual(__FUNCTION__, $result['name']);
+
+ $this->assertNull(Inspector::info('\lithium\util'));
+
+ $result = Inspector::info('\lithium\util\reflection\Inspector');
+ $this->assertTrue(strpos($result['file'], 'lithium/util/reflection/Inspector.php'));
+ $this->assertEqual('lithium\util\reflection', $result['namespace']);
+ $this->assertEqual('Inspector', $result['shortName']);
+
+ $result = Inspector::info('\lithium\util\reflection\Inspector::$_methodMap');
+ $this->assertEqual('_methodMap', $result['name']);
+
+ $expected = 'Maps reflect method names to result array keys.';
+ $this->assertEqual($expected, $result['description']);
+ $this->assertEqual(array('var' => 'array'), $result['tags']);
+
+ $result = Inspector::info('\lithium\util\reflection\Inspector::info()', array(
+ 'modifiers', 'namespace', 'foo'
+ ));
+ $this->assertEqual(array('modifiers', 'namespace'), array_keys($result));
+
+ $this->assertNull(Inspector::info('\lithium\util\reflection\Inspector::$foo'));
+ }
+
+
+ public function testClassDependencies() {
+ $expected = array(
+ 'Exception', 'ReflectionClass', 'lithium\\core\\Libraries', 'lithium\\util\\Collection'
+ );
+ $result = Inspector::dependencies($this->subject());
+ $this->assertEqual($expected, $result);
+
+ $result = Inspector::dependencies($this->subject(), array('type' => 'static'));
+ $this->assertEqual($expected, $result);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/reflection/ParserTest.php b/libraries/lithium/tests/cases/util/reflection/ParserTest.php
new file mode 100644
index 0000000..772b77b
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/reflection/ParserTest.php
@@ -0,0 +1,137 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\util\reflection;
+
+use \lithium\util\reflection\Parser;
+
+class ParserTest extends \lithium\test\Unit {
+
+ /**
+ * Tests that PHP code snippets properly resolve to their corresponding tokens.
+ *
+ * @return void
+ */
+ public function testSingleTokenization() {
+ $result = Parser::token('static');
+ $this->assertEqual('T_STATIC', $result);
+
+ $result = Parser::token('=>');
+ $this->assertEqual('T_DOUBLE_ARROW', $result);
+
+ $result = Parser::token(' =>');
+ $this->assertEqual('T_WHITESPACE', $result);
+
+ $result = Parser::token('static =>');
+ $this->assertEqual('T_STATIC', $result);
+
+ $result = Parser::token("\nstatic =>");
+ $this->assertEqual('T_WHITESPACE', $result);
+
+ $this->assertFalse(Parser::token(''));
+
+ $result = Parser::token(';');
+ $this->assertEqual(';', $result);
+
+ $result = Parser::token('"string"');
+ $this->assertEqual('T_CONSTANT_ENCAPSED_STRING', $result);
+
+ $result = Parser::token('1');
+ $this->assertEqual('T_LNUMBER', $result);
+
+ $result = Parser::token('0');
+ $this->assertEqual('T_LNUMBER', $result);
+
+ $result = Parser::token('0');
+ $this->assertEqual('T_LNUMBER', $result);
+ }
+
+ public function testFullTokenization() {
+ $result = Parser::tokenize('$foo = function() {};');
+ $this->assertEqual(11, count($result));
+
+ $expected = array('id' => 309, 'name' => 'T_VARIABLE', 'content' => '$foo', 'line' => 1);
+ $this->assertEqual($expected, $result[0]);
+
+ $expected = array('id' => null, 'name' => ';', 'content' => ';', 'line' => 1);
+ $this->assertEqual($expected, $result[10]);
+
+ $code = '$defaults = array("id" => "foo", "name" => "bar", \'count\' => 5);';
+ $result = Parser::tokenize($code);
+
+ $this->assertEqual(27, count($result));
+ $this->assertEqual('T_VARIABLE', $result[0]['name']);
+ $this->assertEqual('$defaults', $result[0]['content']);
+ }
+
+ public function testTokenPatternMatching() {
+ $code = '$defaults = array("id" => "foo", "name" => "bar", \'count\' => 5);';
+
+ $result = Parser::match($code, array('"string"'), array('return' => 'content'));
+ $expected = array('"id"', '"foo"', '"name"', '"bar"', '\'count\'');
+ $this->assertEqual($expected, $result);
+
+ $result = Parser::match(
+ $code,
+ array('"string"' => array('before' => '=>'), '1' => array('before' => '=>')),
+ array('return' => 'content')
+ );
+ $expected = array('"foo"', '"bar"', '5');
+ $this->assertEqual($expected, $result);
+
+ $result = Parser::match($code, array('"string"' => array('after' => '=>')), array(
+ 'return' => 'content'
+ ));
+ $expected = array ('"id"', '"name"', '\'count\'');
+ $this->assertEqual($expected, $result);
+ }
+
+ public function testFindingTokenPatterns() {
+ $code = file_get_contents(\lithium\core\Libraries::path('lithium\util\reflection\Parser'));
+
+ $expected = array('tokenize', 'matchToken', '_prepareMatchParams', 'token');
+ $results = array_values(array_unique(array_map(function($i) { return $i[0]; }, Parser::find(
+ $code, 'static::_(*)', array('capture' => array('T_STRING'), 'return' => 'content')
+ ))));
+
+ $this->assertEqual($expected, $results);
+
+ $expected = array(
+ '\ReflectionClass',
+ '\lithium\core\Libraries',
+ '\lithium\util\Collection',
+ '\lithium\util\Validator',
+ '\lithium\util\Set'
+ );
+
+ $results = array_map(
+ function ($i) { return join('', $i); },
+ $results = Parser::find($code, 'use *;', array(
+ 'return' => 'content',
+ 'lineBreaks' => true,
+ 'startOfLine' => true,
+ 'capture' => array('T_STRING', 'T_NS_SEPARATOR')
+ ))
+ );
+ $this->assertEqual($expected, $results);
+
+ $code = 'function test($options) { return function($foo) use ($options) {';
+ $code .= ' ClassName::method($options); ' . "\n" . ' $foo->method($options); }; }';
+ list($results) = Parser::find($code, '_::_(', array(
+ 'capture' => array('T_STRING'), 'return' => 'content'
+ ));
+ $expected = array('ClassName', 'method');
+ $this->assertEqual($expected, $results);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/socket/CurlTest.php b/libraries/lithium/tests/cases/util/socket/CurlTest.php
new file mode 100644
index 0000000..b32b118
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/socket/CurlTest.php
@@ -0,0 +1,96 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\util\socket;
+
+class CurlMock extends \lithium\util\socket\Curl {
+
+ public function resource() {
+ return $this->_resource;
+ }
+}
+
+class CurlTest extends \lithium\test\Unit {
+
+ protected $_testConfig = array(
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 2
+ );
+
+ public function testAllMethodsNoConnection() {
+ $stream = new CurlMock(array('protocol' => null));
+ $this->assertFalse($stream->open());
+ $this->assertTrue($stream->close());
+ $this->assertFalse($stream->timeout(2));
+ $this->assertFalse($stream->encoding('UTF-8'));
+ $this->assertFalse($stream->write(null));
+ $this->assertFalse($stream->read());
+ }
+
+ public function testOpen() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->open();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testClose() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->open();
+ $this->assertTrue($result);
+
+ $result = $stream->close();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertFalse(is_resource($result));
+ }
+
+ public function testTimeout() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->open();
+ $stream->timeout(10);
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testEncoding() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->open();
+ $stream->encoding('UTF-8');
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testWriteAndRead() {
+ $stream = new CurlMock($this->_testConfig);
+ $result = $stream->open();
+ $this->assertTrue(is_resource($result));
+
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+
+ $stream->set(CURLOPT_URL, 'http://localhost');
+ $this->assertTrue($stream->write(null));
+
+ $result = $stream->read();
+ $this->assertPattern("/^<!DOCTYPE/", $result);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/tests/cases/util/socket/StreamTest.php b/libraries/lithium/tests/cases/util/socket/StreamTest.php
new file mode 100644
index 0000000..db3737a
--- /dev/null
+++ b/libraries/lithium/tests/cases/util/socket/StreamTest.php
@@ -0,0 +1,93 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\tests\cases\util\socket;
+
+class StreamMock extends \lithium\util\socket\Stream {
+
+ public function resource() {
+ return $this->_resource;
+ }
+}
+
+class StreamTest extends \lithium\test\Unit {
+
+ protected $_testConfig = array(
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 2
+ );
+
+ public function testAllMethodsNoConnection() {
+ $stream = new StreamMock(array('protocol' => null));
+ $this->assertFalse($stream->open());
+ $this->assertTrue($stream->close());
+ $this->assertFalse($stream->timeout(2));
+ $this->assertFalse($stream->encoding('UTF-8'));
+ $this->assertFalse($stream->write(null));
+ $this->assertFalse($stream->read());
+ }
+
+ public function testOpen() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->open();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testClose() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->open();
+ $this->assertTrue($result);
+
+ $result = $stream->close();
+ $this->assertTrue($result);
+
+ $result = $stream->resource();
+ $this->assertFalse(is_resource($result));
+ }
+
+ public function testTimeout() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->open();
+ $stream->timeout(10);
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testEncoding() {
+ $stream = new StreamMock($this->_testConfig);
+ $result = $stream->open();
+ $stream->encoding('UTF-8');
+ $result = $stream->resource();
+ $this->assertTrue(is_resource($result));
+ }
+
+ public function testWriteAndRead() {
+ $stream = new StreamMock($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));
+
+ $result = $stream->read();
+ $this->assertPattern("/^HTTP/", $result);
+ }
+}
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/Collection.php b/libraries/lithium/util/Collection.php
new file mode 100644
index 0000000..8f8b762
--- /dev/null
+++ b/libraries/lithium/util/Collection.php
@@ -0,0 +1,222 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util;
+
+class Collection extends \lithium\core\Object implements \ArrayAccess, \Iterator, \Countable {
+
+ protected $_items = array();
+
+ protected $_valid = false;
+
+ protected $_autoConfig = array('items');
+
+ protected function _init() {
+ parent::_init();
+ unset($this->_config['items']);
+ }
+
+ /**
+ * Handles dispatching of methods against all items in the collection.
+ *
+ * @param string $method
+ * @param array $parameters
+ * @param array $options Specifies options for how to run the given method against the object
+ * collection. The available options are:
+ * - 'merge': Used primarily if the method being invoked returns an array. If
+ * set to true, merges all results arrays into one.
+ * - 'collect': If true, the results of this method call will be returned wrapped
+ * in a new Collection object or subclass.
+ * @todo Implement filtering
+ * @return mixed
+ */
+ public function invoke($method, $parameters = array(), $options = array()) {
+ $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;
+ }
+ }
+
+ if ($options['collect']) {
+ $class = get_class($this);
+ $results = new $class(array('items' => $results));
+ }
+ return $results;
+ }
+
+ /**
+ * Hook to handle dispatching of methods against all items in the collection.
+ *
+ * @param string $method
+ * @param array $parameters
+ * @return mixed
+ */
+ public function __call($method, $parameters = array()) {
+ return $this->invoke($method, $parameters);
+ }
+
+ /**
+ * Converts the Collection object to another type of object, or a simple type such as an array.
+ *
+ * @param string $format
+ * @return mixed
+ */
+ public function to($format, $options = array()) {
+ switch ($format) {
+ case 'array':
+ $result = array();
+
+ foreach ($this->_items as $key => $value) {
+ if (is_object($value)) {
+ if (method_exists($value, 'to')) {
+ $value = $value->to('array');
+ }
+ if (!is_array($value)) {
+ $value = get_object_vars($value);
+ }
+ }
+ $result[$key] = $value;
+ }
+ break;
+ }
+ return $result;
+ }
+
+ public function find($filter, $options = array()) {
+ $defaults = array('collect' => true);
+ $options += $defaults;
+ $items = array_filter($this->_items, $filter);
+
+ if ($options['collect']) {
+ $class = get_class($this);
+ $items = new $class(compact('items'));
+ }
+ return $items;
+ }
+
+ /**
+ * Returns the first non-empty value in a collection after a filter is applied.
+ *
+ * @param closure $filter The filter 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.
+ * @return mixed Returns the first non-empty collection value returned from `$filter`.
+ */
+ public function first($filter) {
+ foreach ($this->_items as $item) {
+ if ($value = $filter($item)) {
+ return $value;
+ }
+ }
+ }
+
+ public function each($filter) {
+ $this->_items = array_map($filter, $this->_items);
+ return $this;
+ }
+
+ public function map($filter, $options = array()) {
+ $defaults = array('collect' => true);
+ $options += $defaults;
+ $items = array_map($filter, $this->_items);
+
+ if ($options['collect']) {
+ $class = get_class($this);
+ return new $class(compact('items'));
+ }
+ return $items;
+ }
+
+ public function offsetExists($offset) {
+ return isset($this->_items[$offset]);
+ }
+
+ public function offsetGet($offset) {
+ return $this->_items[$offset];
+ }
+
+ public function offsetSet($offset, $value) {
+ if (is_null($offset)) {
+ return $this->_items[] = $value;
+ }
+ return $this->_items[$offset] = $value;
+ }
+
+ public function offsetUnset($offset) {
+ unset($this->_items[$offset]);
+ }
+
+ public function rewind() {
+ $this->_valid = (reset($this->_items) !== false);
+ return current($this->_items);
+ }
+
+ public function end() {
+ $this->_valid = (end($this->_items) !== false);
+ return current($this->_items);
+ }
+
+ public function valid() {
+ return $this->_valid;
+ }
+
+ public function current() {
+ return current($this->_items);
+ }
+
+ public function key() {
+ return key($this->_items);
+ }
+
+ public function prev() {
+ if (!prev($this->_items)) {
+ end($this->_items);
+ }
+ return current($this->_items);
+ }
+
+ public function next() {
+ $this->_valid = (next($this->_items) !== false);
+ return current($this->_items);
+ }
+
+ public function append($value) {
+ is_object($value) ? $this->_items[] =& $value : $this->_items[] = $value;
+ }
+
+ public function count() {
+ return count($this->_items);
+ }
+
+ public function keys() {
+ return array_keys($this->_items);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/Inflector.php b/libraries/lithium/util/Inflector.php
new file mode 100644
index 0000000..393f4d8
--- /dev/null
+++ b/libraries/lithium/util/Inflector.php
@@ -0,0 +1,449 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util;
+
+/**
+ * Pluralize and singularize English words.
+ *
+ * Inflector pluralizes and singularizes English nouns.
+ * Used by Lithium's naming conventions throughout the framework.
+ *
+ * @package lithium
+ * @subpackage lithium.lithium.libs
+ */
+class Inflector {
+
+ /**
+ * Contians a default map of accented and special characters to ASCII characters. Can be
+ * extended or added to using `Inflector::rules()`.
+ *
+ * @var array
+ * @see lithium\util\Inflector::slug()
+ * @see lithium\util\Inflector::rules()
+ */
+ protected static $_transliterations = array(
+ '/à|á|å|â/' => 'a',
+ '/è|é|ê|ẽ|ë/' => 'e',
+ '/ì|í|î/' => 'i',
+ '/ò|ó|ô|ø/' => 'o',
+ '/ù|ú|ů|û/' => 'u',
+ '/ç/' => 'c',
+ '/ñ/' => 'n',
+ '/ä|æ/' => 'ae',
+ '/ö/' => 'oe',
+ '/ü/' => 'ue',
+ '/Ä/' => 'Ae',
+ '/Ü/' => 'Ue',
+ '/Ö/' => 'Oe',
+ '/ß/' => 'ss'
+ );
+
+ /**
+ * Indexed array of words which are the same in both singular and plural form. You can add
+ * rules to this list using Inflector::rules().
+ *
+ * @var array
+ * @see lithium\util\Inflector::rules()
+ */
+ protected static $_uninflected = array(
+ 'Amoyese', 'bison', 'Borghese', 'bream', 'breeches', 'britches', 'buffalo', 'cantus',
+ 'carp', 'chassis', 'clippers', 'cod', 'coitus', 'Congoese', 'contretemps', 'corps',
+ 'debris', 'diabetes', 'djinn', 'eland', 'elk', 'equipment', 'Faroese', 'flounder',
+ 'Foochowese', 'gallows', 'Genevese', 'Genoese', 'Gilbertese', 'graffiti',
+ 'headquarters', 'herpes', 'hijinks', 'Hottentotese', 'information', 'innings',
+ 'jackanapes', 'Kiplingese', 'Kongoese', 'Lucchese', 'mackerel', 'Maltese', 'media',
+ 'mews', 'moose', 'mumps', 'Nankingese', 'news', 'nexus', 'Niasese', 'People',
+ 'Pekingese', 'Piedmontese', 'pincers', 'Pistoiese', 'pliers', 'Portuguese',
+ 'proceedings', 'rabies', 'rice', 'rhinoceros', 'salmon', 'Sarawakese', 'scissors',
+ 'sea[- ]bass', 'series', 'Shavese', 'shears', 'siemens', 'species', 'swine', 'testes',
+ 'trousers', 'trout','tuna', 'Vermontese', 'Wenchowese', 'whiting', 'wildebeest',
+ 'Yengeese'
+ );
+
+ /**
+ * Contains the list of pluralization rules. Contains the following keys:
+ * - 'rules': An array of regular expression rules in the form of 'match' => 'replace',
+ * which specify the matching and replacing rules for the pluralization of words.
+ * - 'uninflected': A indexed array containing regex word patterns which do not get
+ * inflected (i.e. singular and plural are the same).
+ * - 'irregular': Contains key-value pairs of specific words which are not inflected
+ * according to the rules (this is populated from Inflector::$_plural when the class
+ * is loaded).
+ *
+ * @var array
+ * @see lithium\util\Inflector::rules()
+ */
+ protected static $_singular = array(
+ 'rules' => array(
+ '/(s)tatuses$/i' => '\1\2tatus',
+ '/^(.*)(menu)s$/i' => '\1\2',
+ '/(quiz)zes$/i' => '\\1',
+ '/(matr)ices$/i' => '\1ix',
+ '/(vert|ind)ices$/i' => '\1ex',
+ '/^(ox)en/i' => '\1',
+ '/(alias)(es)*$/i' => '\1',
+ '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
+ '/(cris|ax|test)es$/i' => '\1is',
+ '/(shoe)s$/i' => '\1',
+ '/(o)es$/i' => '\1',
+ '/ouses$/' => 'ouse',
+ '/uses$/' => 'us',
+ '/([m|l])ice$/i' => '\1ouse',
+ '/(x|ch|ss|sh)es$/i' => '\1',
+ '/(m)ovies$/i' => '\1\2ovie',
+ '/(s)eries$/i' => '\1\2eries',
+ '/([^aeiouy]|qu)ies$/i' => '\1y',
+ '/([lr])ves$/i' => '\1f',
+ '/(tive)s$/i' => '\1',
+ '/(hive)s$/i' => '\1',
+ '/(drive)s$/i' => '\1',
+ '/([^fo])ves$/i' => '\1fe',
+ '/(^analy)ses$/i' => '\1sis',
+ '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i' => '\1\2sis',
+ '/([ti])a$/i' => '\1um',
+ '/(p)eople$/i' => '\1\2erson',
+ '/(m)en$/i' => '\1an',
+ '/(c)hildren$/i' => '\1\2hild',
+ '/(n)ews$/i' => '\1\2ews',
+ '/^(.*us)$/' => '\\1',
+ '/s$/i' => ''
+ ),
+ 'irregular' => array(),
+ 'uninflected' => array(
+ '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep', '.*ss'
+ )
+ );
+
+ /**
+ * Contains a cache map of previously singularized words.
+ *
+ * @var array
+ */
+ protected static $_singularized = array();
+
+ /**
+ * Contains the list of pluralization rules. Contains the following keys:
+ * - 'rules': An array of regular expression rules in the form of 'match' => 'replace',
+ * which specify the matching and replacing rules for the pluralization of words.
+ * - 'uninflected': A indexed array containing regex word patterns which do not get
+ * inflected (i.e. singular and plural are the same).
+ * - 'irregular': Contains key-value pairs of specific words which are not inflected
+ * according to the rules.
+ *
+ * @var array
+ * @see lithium\util\Inflector::rules()
+ */
+ protected static $_plural = array(
+ 'rules' => array(
+ '/(s)tatus$/i' => '\1\2tatuses',
+ '/(quiz)$/i' => '\1zes',
+ '/^(ox)$/i' => '\1\2en',
+ '/([m|l])ouse$/i' => '\1ice',
+ '/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
+ '/(x|ch|ss|sh)$/i' => '\1es',
+ '/([^aeiouy]|qu)y$/i' => '\1ies',
+ '/(hive)$/i' => '\1s',
+ '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
+ '/sis$/i' => 'ses',
+ '/([ti])um$/i' => '\1a',
+ '/(p)erson$/i' => '\1eople',
+ '/(m)an$/i' => '\1en',
+ '/(c)hild$/i' => '\1hildren',
+ '/(buffal|tomat)o$/i' => '\1\2oes',
+ '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i',
+ '/us$/' => 'uses',
+ '/(alias)$/i' => '\1es',
+ '/(ax|cri|test)is$/i' => '\1es',
+ '/s$/' => 's',
+ '/^$/' => '',
+ '/$/' => 's'
+ ),
+ 'irregular' => array(
+ 'atlas' => 'atlases', 'beef' => 'beefs', 'brother' => 'brothers',
+ 'child' => 'children', 'corpus' => 'corpuses', 'cow' => 'cows',
+ 'ganglion' => 'ganglions', 'genie' => 'genies', 'genus' => 'genera',
+ 'graffito' => 'graffiti', 'hoof' => 'hoofs', 'loaf' => 'loaves', 'man' => 'men',
+ 'money' => 'monies', 'mongoose' => 'mongooses', 'move' => 'moves',
+ 'mythos' => 'mythoi', 'numen' => 'numina', 'occiput' => 'occiputs',
+ 'octopus' => 'octopuses', 'opus' => 'opuses', 'ox' => 'oxen', 'penis' => 'penises',
+ 'person' => 'people', 'sex' => 'sexes', 'soliloquy' => 'soliloquies',
+ 'testis' => 'testes', 'trilby' => 'trilbys', 'turf' => 'turfs'
+ ),
+ 'uninflected' => array(
+ '.*[nrlm]ese', '.*deer', '.*fish', '.*measles', '.*ois', '.*pox', '.*sheep'
+ )
+ );
+
+ /**
+ * Contains a cache map of previously pluralized words.
+ *
+ * @var array
+ */
+ protected static $_pluralized = array();
+
+ /**
+ * Populates Inflector::$_singular['irregular'] as an inversion of
+ * Inflector::$_plural['irregular'].
+ *
+ * @return void
+ */
+ public static function __init() {
+ static::$_singular['irregular'] = array_flip(static::$_plural['irregular']);
+ }
+
+ /**
+ * Gets or adds inflection and transliteration rules.
+ *
+ * @param string $type
+ * @param array $config
+ * @return mixed If $rules is empty, returns the rules list specified by $type, otherwise
+ * returns null.
+ */
+ public static function rules($type, $config = array()) {
+ $var = '_' . $type;
+
+ if (!isset(static::${$var})) {
+ return null;
+ }
+
+ if (empty($config)) {
+ return static::${$var};
+ }
+
+ switch ($type) {
+ case 'transliterations':
+ $_config = array();
+
+ foreach ($config as $key => $val) {
+ if ($key[0] != '/') {
+ $key = '/' . join('|', array_filter(preg_split('//u', $key))) . '/';
+ }
+ $_config[$key] = $val;
+ }
+ static::$_transliterations = array_merge(
+ $_config, static::$_transliterations, $_config
+ );
+ break;
+ case 'uninflected':
+ static::$_uninflected = array_merge(static::$_uninflected, (array)$config);
+ static::$_plural['regexUninflected'] = null;
+ static::$_singular['regexUninflected'] = null;
+
+ foreach ((array)$config as $word) {
+ unset(static::$_singularized[$word], static::$_pluralized[$word]);
+ }
+ break;
+ case 'singular':
+ case 'plural':
+ if (isset(static::${$var}[key($config)])) {
+ foreach ($config as $rType => $set) {
+ static::${$var}[$rType] = array_merge($set, static::${$var}[$rType], $set);
+
+ if ($rType == 'irregular') {
+ $swap = ($type == 'singular' ? '_plural' : '_singular');
+ static::${$swap}[$rType] = array_flip(static::${$var}[$rType]);
+ }
+ }
+ } else {
+ static::${$var}['rules'] = array_merge(
+ $config, static::${$var}['rules'], $config
+ );
+ }
+ break;
+ }
+ }
+
+ /**
+ * Return $word in plural form.
+ *
+ * @param string $word Word in singular
+ * @return string Word in plural
+ */
+ public static function pluralize($word) {
+ if (array_key_exists($word, static::$_pluralized)) {
+ return static::$_pluralized[$word];
+ }
+ extract(static::$_plural);
+
+ if (!isset($regexUninflected) || !isset($regexIrregular)) {
+ $regexUninflected = static::_enclose(join( '|', $uninflected + static::$_uninflected));
+ $regexIrregular = static::_enclose(join( '|', array_keys($irregular)));
+ static::$_plural += compact('regexUninflected', 'regexIrregular');
+ }
+
+ if (preg_match('/^(' . $regexUninflected . ')$/i', $word, $regs)) {
+ return static::$_pluralized[$word] = $word;
+ }
+
+ if (preg_match('/(.*)\\b(' . $regexIrregular . ')$/i', $word, $regs)) {
+ $plural = substr($word, 0, 1) . substr($irregular[strtolower($regs[2])], 1);
+ static::$_pluralized[$word] = $regs[1] . $plural;
+ return static::$_pluralized[$word];
+ }
+
+ foreach ($rules as $rule => $replacement) {
+ if (preg_match($rule, $word)) {
+ return static::$_pluralized[$word] = preg_replace($rule, $replacement, $word);
+ }
+ }
+ return static::$_pluralized[$word] = $word;
+ }
+
+ /**
+ * Return $word in singular form.
+ *
+ * @param string $word Word in plural
+ * @return string Word in singular
+ */
+ public static function singularize($word) {
+ if (array_key_exists($word, static::$_singularized)) {
+ return static::$_singularized[$word];
+ }
+ extract(static::$_singular);
+
+ if (!isset($regexUninflected) || !isset($regexIrregular)) {
+ $regexUninflected = static::_enclose(join('|', $uninflected + static::$_uninflected));
+ $regexIrregular = static::_enclose(join('|', array_keys($irregular)));
+ static::$_singular += compact('regexUninflected', 'regexIrregular');
+ }
+
+ if (preg_match('/(.*)\\b(' . $regexIrregular . ')$/i', $word, $regs)) {
+ $singular = substr($word, 0, 1) . substr($irregular[strtolower($regs[2])], 1);
+ static::$_singularized[$word] = $regs[1] . $singular;
+ } elseif (preg_match('/^(' . $regexUninflected . ')$/i', $word, $regs)) {
+ static::$_singularized[$word] = $word;
+ } else {
+ foreach ($rules as $rule => $replacement) {
+ if (preg_match($rule, $word)) {
+ static::$_singularized[$word] = preg_replace($rule, $replacement, $word);
+ break;
+ }
+ }
+ }
+
+ if (!array_key_exists($word, static::$_singularized)) {
+ static::$_singularized[$word] = $word;
+ }
+ return static::$_singularized[$word];
+ }
+
+ /**
+ * Clears local in-memory caches. Can be used to force a full-cache clear when updating
+ * inflection rules mid-way through request execution.
+ *
+ * @return void
+ */
+ public static function clear() {
+ static::$_singularized = static::$_pluralized = array();
+ static::$_plural['regexUninflected'] = static::$_singular['regexUninflected'] = null;
+ static::$_plural['regexIrregular'] = static::$_singular['regexIrregular'] = null;
+ }
+
+ /**
+ * Returns given $lower_case_and_underscored_word as a CamelCased word.
+ *
+ * @param string $lowerCaseAndUnderscoredWord Word to camelize
+ * @return string Camelized word. LikeThis.
+ */
+ public static function camelize($lowerCaseAndUnderscoredWord) {
+ return str_replace(" ", "", ucwords(str_replace("_", " ", $lowerCaseAndUnderscoredWord)));
+ }
+
+ /**
+ * Returns an underscore-syntaxed (like_this_dear_reader) version of the $camelCasedWord.
+ *
+ * @param string $camelCasedWord Camel-cased word to be "underscorized"
+ * @return string Underscore-syntaxed version of the $camelCasedWord
+ */
+ public static function underscore($camelCasedWord) {
+ return strtolower(preg_replace('/(?<=\\w)([A-Z])/', '_\\1', $camelCasedWord));
+ }
+
+ /**
+ * Returns a human-readable string from $lowerCaseAndUnderscoredWord,
+ * by replacing underscores with a space, and by upper-casing the initial characters.
+ *
+ * @param string $lowerCaseAndUnderscoredWord String to be made more readable.
+ * @param string $separator The separator character used in the initial string
+ * @return string Human-readable string.
+ */
+ public static function humanize($lowerCaseAndUnderscoredWord, $separator = '_') {
+ return ucwords(str_replace($separator, " ", $lowerCaseAndUnderscoredWord));
+ }
+
+ /**
+ * Returns corresponding table name for given $class_name. ("posts" for the
+ * model class "Post").
+ *
+ * @param string $className Name of class to get database table name for
+ * @return string Name of the database table for given class
+ */
+ public static function tableize($className) {
+ return static::pluralize(static::underscore($className));
+ }
+
+ /**
+ * Returns Lithium model class name ("Post" for the database table "posts".) for the
+ * given database table.
+ *
+ * @param string $tableName Name of database table to get class name for
+ * @return string
+ */
+ public static function classify($tableName) {
+ return static::camelize(static::singularize($tableName));
+ }
+
+ /**
+ * Returns camelBacked version of a string.
+ *
+ * @param string $string
+ * @return string
+ */
+ public static function variable($string) {
+ $string = static::camelize(static::underscore($string));
+ $replace = strtolower(substr($string, 0, 1));
+ $variable = preg_replace('/\\w/', $replace, $string, 1);
+ return $variable;
+ }
+
+ /**
+ * Returns a string with all spaces converted to $replacement and non word characters removed.
+ * Maps special characters to ASCII using Inflector::$_transliterations, which can be updated
+ * using Inflector::rules().
+ *
+ * @param string $string
+ * @param string $replacement
+ * @return string
+ * @see lithium\util\Inflector::rules()
+ */
+ public static function slug($string, $replacement = '_') {
+ $map = static::$_transliterations + 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
+ * @return string Enclosed string
+ */
+ protected static function _enclose($string) {
+ return '(?:' . $string . ')';
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/Set.php b/libraries/lithium/util/Set.php
new file mode 100644
index 0000000..f9d9b05
--- /dev/null
+++ b/libraries/lithium/util/Set.php
@@ -0,0 +1,898 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util;
+
+class Set {
+
+ /**
+ * Checks if a particular path is set in an array
+ *
+ * @param mixed $data Data to check on
+ * @param mixed $path A dot-separated string.
+ * @return boolean true if path is found, false otherwise
+ */
+ public static function check($data, $path = null) {
+ if (empty($path)) {
+ return $data;
+ }
+ if (!is_array($path)) {
+ $path = explode('.', $path);
+ }
+
+ foreach ($path as $i => $key) {
+ if (is_numeric($key) && intval($key) > 0 || $key === '0') {
+ $key = intval($key);
+ }
+ if ($i === count($path) - 1) {
+ return (is_array($data) && array_key_exists($key, $data));
+ } else {
+ if (!is_array($data) || !array_key_exists($key, $data)) {
+ return false;
+ }
+ $data =& $data[$key];
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Counts the dimensions of an array. If $all is set to false (which is the default) it will
+ * only consider the dimension of the first element in the array.
+ *
+ * @param array $array Array to count dimensions on
+ * @param boolean $all true counts the dimension considering all elements in array
+ * @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)) {
+ return 0;
+ }
+ if (!is_array($options)) {
+ $options = array('all' => $options, 'count' => $count);
+ }
+ $defaults = array('all' => false, 'count' => 0);
+ $options += $defaults;
+ 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
+ ));
+ }
+ }
+ return max($depth);
+ }
+ if (is_array(reset($data))) {
+ return static::depth(reset($data)) + 1;
+ }
+ return 1;
+ }
+
+ /**
+ * Collapses a multi-dimensional array into a single dimension, using a delimited array path
+ * for each array element's key, i.e. array(array('Foo' => array('Bar' => 'Far'))) becomes
+ * array('0.Foo.Bar' => 'Far').
+ *
+ * @param array $data array to flatten
+ * @param array $options
+ * - separator : string to separate array keys in path [default] .
+ * - path starting point [default] null
+ * @return array
+ */
+ public static function flatten($data, $options = array()) {
+ $result = array();
+
+ if (!is_array($options)) {
+ $options = array('separator' => $options);
+ }
+
+ $defaults = array('separator' => '.', 'path' => null);
+ $options += $defaults;
+
+ 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 {
+ $result[$options['path'] . $key] = $val;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Filters empty elements out of a route array, excluding '0'.
+ *
+ * @param mixed $var Either an array to filter, or value when in callback
+ * @return mixed Either filtered array, or true/false when in callback
+ */
+ public static function filter($var) {
+ if (!is_array($var)) {
+ $var = array($var);
+ }
+ return array_filter($var, function($var) {
+ return ($var === 0 || $var === '0' || !empty($var));
+ });
+ }
+
+ /**
+ * Returns a series of values extracted from an array, formatted in a format string.
+ *
+ * @param array $data Source array from which to extract the data
+ * @param string $format Format string into which values will be inserted, see sprintf()
+ * @param array $keys An array containing one or more Set::extract()-style key paths
+ * @return array An array of strings extracted from $keys and formatted with $format
+ */
+ public static function format($data, $format, $keys) {
+
+ $extracted = array();
+ $count = count($keys);
+
+ if (!$count) {
+ return;
+ }
+
+ for ($i = 0; $i < $count; $i++) {
+ $extracted[] = static::extract($data, $keys[$i]);
+ }
+ $out = array();
+ $data = $extracted;
+ $count = count($data[0]);
+
+ if (preg_match_all('/\{([0-9]+)\}/msi', $format, $keys2) && isset($keys2[1])) {
+ $keys = $keys2[1];
+ $format = preg_split('/\{([0-9]+)\}/msi', $format);
+ $count2 = count($format);
+
+ for ($j = 0; $j < $count; $j++) {
+ $formatted = '';
+ for ($i = 0; $i <= $count2; $i++) {
+ if (isset($format[$i])) {
+ $formatted .= $format[$i];
+ }
+ if (isset($keys[$i]) && isset($data[$keys[$i]][$j])) {
+ $formatted .= $data[$keys[$i]][$j];
+ }
+ }
+ $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];
+ }
+ }
+ $out[] = vsprintf($format, $args);
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Checks to see if all the values in the array are numeric
+ *
+ * @param array $array The array to check. If null, the value of the current Set object
+ * @return boolean true if values are numeric, false otherwise
+ */
+ public static function isNumeric($array = null) {
+ if (empty($array)) {
+ return null;
+ }
+
+ if ($array === range(0, count($array) - 1)) {
+ return true;
+ }
+
+ $numeric = true;
+ $keys = array_keys($array);
+ $count = count($keys);
+
+ for ($i = 0; $i < $count; $i++) {
+ if (!is_numeric($array[$keys[$i]])) {
+ $numeric = false;
+ break;
+ }
+ }
+ return $numeric;
+ }
+
+ /**
+ * 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 integer $i Optional: The 'nth'-number of the item being matched.
+ * @return boolean
+ */
+ public static function matches($conditions, $data = array(), $i = null, $length = null) {
+ if (empty($conditions)) {
+ return true;
+ }
+ if (is_string($conditions) || is_string($data)) {
+ return !!static::extract($data, $conditions);
+ }
+ // var_dump($conditions);
+ // var_dump($data);
+ foreach ($conditions as $condition) {
+ if ($condition === ':last') {
+ if ($i != $length) {
+ return false;
+ }
+ continue;
+ } elseif ($condition === ':first') {
+ if ($i != 1) {
+ return false;
+ }
+ continue;
+ }
+ if (!preg_match('/(.+?)([><!]?[=]|[><])(.*)/', $condition, $match)) {
+ if (ctype_digit($condition)) {
+ if ($i != $condition) {
+ return false;
+ }
+ } elseif (preg_match_all('/(?:^[0-9]+|(?<=,)[0-9]+)/', $condition, $matches)) {
+ return in_array($i, $matches[0]);
+ } elseif (!array_key_exists($condition, $data)) {
+ return false;
+ }
+ continue;
+ }
+
+ list(,$key,$op,$expected) = $match;
+ if (!isset($data[$key])) {
+ return false;
+ }
+ $val = $data[$key];
+
+ if ($op === '=' && $expected && $expected{0} === '/') {
+ return preg_match($expected, $val);
+ } elseif ($op === '=' && $val != $expected) {
+ return false;
+ } elseif ($op === '!=' && $val == $expected) {
+ return false;
+ } elseif ($op === '>' && $val <= $expected) {
+ return false;
+ } elseif ($op === '<' && $val >= $expected) {
+ return false;
+ } elseif ($op === '<=' && $val > $expected) {
+ return false;
+ } elseif ($op === '>=' && $val < $expected) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 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 $class A class name of the type of object to map to
+ * @param boolean $name whether the _name_ should be filled
+ * @return object Hierarchical object
+ */
+ public static function map($data, $class = 'stdClass', $name = false) {
+ if (empty($data)) {
+ return $data;
+ }
+ if ($class === true) {
+ $out = new \stdClass;
+ } else {
+ $out = new $class;
+ }
+
+ if (is_array($data)) {
+ $keys = array_keys($data);
+ foreach ($data as $key => $value) {
+ if ($keys[0] === $key && $class !== true) {
+ $name = true;
+ }
+ if (is_numeric($key)) {
+ if (is_object($out)) {
+ $out = get_object_vars($out);
+ }
+ $out[$key] = static::map($value, $class);
+ $isNamed = (
+ is_object($out[$key]) && !isset($out[$key]->_name_) &&
+ $name !== true && static::depth($value, true) >= 2
+ );
+ if ($isNamed) {
+ $out[$key]->_name_ = $name;
+ }
+ } elseif (is_array($value)) {
+ if ($name === true) {
+ $out->_name_ = $key;
+ $name = false;
+ foreach ($value as $key2 => $value2) {
+ $out->{$key2} = static::map($value2, true);
+ }
+ } else {
+ if (!is_numeric($key)) {
+ $out->{$key} = static::map($value, true, $key);
+ if (is_object($out->{$key}) && !isset($out->{$key}->_name_)) {
+ $out->{$key}->_name_ = $key;
+ }
+ } else {
+ $out->{$key} = static::map($value, true);
+ }
+ }
+ } else {
+ $out->{$key} = $value;
+ }
+ }
+ } else {
+ $out = $data;
+ }
+ return $out;
+ }
+
+ /**
+ * This function can be thought of as a hybrid between PHP's array_merge and
+ * array_merge_recursive. The difference to the two is that if an array key contains another
+ * array then the function behaves recursive (unlike array_merge) but does not do if for keys
+ * containing strings (unlike array_merge_recursive). See the unit test for more information.
+ *
+ * Note: This function will work with an unlimited amount of arguments and typecasts non-array
+ * parameters into arrays.
+ *
+ * @param array $arr1 Array to be merged
+ * @param array $arr2 Array to merge with
+ * @return array Merged array
+ */
+ public static function merge($arr1, $arr2 = null) {
+ $args = func_get_args();
+
+ if (!isset($r)) {
+ $r = (array)current($args);
+ }
+
+ while (($arg = next($args)) !== false) {
+ foreach ((array)$arg as $key => $val) {
+ if (is_array($val) && isset($r[$key]) && is_array($r[$key])) {
+ $r[$key] = static::merge($r[$key], $val);
+ } elseif (is_int($key)) {
+ $r[] = $val;
+ } else {
+ $r[$key] = $val;
+ }
+ }
+ }
+ return $r;
+ }
+
+ /**
+ * Pushes the differences in $array2 onto the end of $array
+ *
+ * @param mixed $array Original array
+ * @param mixed $array2 Differences to push
+ * @return array Combined array
+ */
+ public static function pushDiff($array, $array2) {
+ if (empty($array) && !empty($array2)) {
+ return $array2;
+ }
+ if (!empty($array) && !empty($array2)) {
+ foreach ($array2 as $key => $value) {
+ if (!array_key_exists($key, $array)) {
+ $array[$key] = $value;
+ } else {
+ if (is_array($value)) {
+ $array[$key] = static::pushDiff($array[$key], $array2[$key]);
+ }
+ }
+ }
+ }
+ return $array;
+ }
+
+ /**
+ * Implements partial support for XPath 2.0. If $path is an array or $data is empty it the
+ * call is delegated to Set::classicExtract.
+ *
+ * Currently implemented selectors:
+ * - /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)
+ * - /User[id>2][<5] (selects all Users with an id > 2 but < 5)
+ * - /Post/Comment[author_name=john]/../name (Selects the name of all Posts that have at least
+ * one Comment written by john)
+ * - /Posts[name] (Selects all Posts that have a 'name' key)
+ * - /Comment/.[1] (Selects the contents of the first comment)
+ * - /Comment/.[:last] (Selects the last comment)
+ * - /Comment/.[:first] (Selects the first comment)
+ * - /Comment[text=/lithiumphp/i] (Selects the all comments that have a text matching
+ * the regex /lithiumphp/i)
+ * - /Comment/@* (Selects the all key names of all comments)
+ *
+ * Other limitations:
+ * - Only absolute paths starting with a single '/' are supported right now
+ *
+ * Warning: Even so it has plenty of unit tests the XPath support has not gone through a lot of
+ * real-world testing. Please report bugs as you find them. Suggestions for additional features
+ * to implement are also very welcome!
+ *
+ * @param string $data An absolute XPath 2.0 path
+ * @param string $path An array of data to extract from
+ * @param string $options Currently only supports 'flatten' which can be disabled
+ * for higher XPath-ness
+ * @return array An array of matched items
+ */
+ public static function extract($data, $path = null, $options = array()) {
+ if (empty($data)) {
+ return array();
+ }
+ if (is_string($data)) {
+ $tmp = $path;
+ $path = $data;
+ $data = $tmp;
+ unset($tmp);
+ }
+ if ($path === '/') {
+ return static::filter($data);
+ }
+ $contexts = $data;
+ $options = array_merge(array('flatten' => true), $options);
+ if (!isset($contexts[0])) {
+ $contexts = array($data);
+ }
+ $tokens = array_slice(preg_split('/(?<!=)\/(?![a-z-]*\])/', $path), 1);
+
+ 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);
+ }
+ if ($token === '..') {
+ if (count($context['trace']) == 1) {
+ $context['trace'][] = $context['key'];
+ }
+ $parent = join('/', $context['trace']) . '/.';
+ $context['item'] = static::extract($parent, $data);
+ $context['key'] = array_pop($context['trace']);
+ if (isset($context['trace'][1]) && $context['trace'][1] > 0) {
+ $context['item'] = $context['item'][0];
+ } elseif (!empty($context['item'][$key])) {
+ $context['item'] = $context['item'][$key];
+ } else {
+ $context['item'] = array_shift($context['item']);
+ }
+ $matches[] = $context;
+ continue;
+ }
+ $match = false;
+ if ($token === '@*' && is_array($context['item'])) {
+ $matches[] = array(
+ 'trace' => array_merge($context['trace'], (array)$key),
+ 'key' => $key,
+ 'item' => array_keys($context['item']),
+ );
+ } elseif (is_array($context['item']) && array_key_exists($token, $context['item'])) {
+ $items = $context['item'][$token];
+ if (!is_array($items)) {
+ $items = array($items);
+ } elseif (!isset($items[0])) {
+ $current = current($items);
+ if ((is_array($current) && count($items) <= 1) || !is_array($current)) {
+ $items = array($items);
+ }
+ }
+
+ foreach ($items as $key => $item) {
+ $ctext = array($context['key']);
+ if (!is_numeric($key)) {
+ $ctext[] = $token;
+ $token = array_shift($tokens);
+ if (isset($items[$token])) {
+ $ctext[] = $token;
+ $item = $items[$token];
+ $matches[] = array(
+ 'trace' => array_merge($context['trace'], $ctext),
+ 'key' => $key,
+ 'item' => $item,
+ );
+ break;
+ } else {
+ array_unshift($tokens, $token);
+ }
+ } else {
+ $key = $token;
+ }
+
+ $matches[] = array(
+ 'trace' => array_merge($context['trace'], $ctext),
+ 'key' => $key,
+ 'item' => $item,
+ );
+ }
+ } elseif (($key === $token || (ctype_digit($token) && $key == $token) || $token === '.')) {
+ $context['trace'][] = $key;
+ $matches[] = array(
+ 'trace' => $context['trace'],
+ 'key' => $key,
+ 'item' => $context['item'],
+ );
+ }
+ }
+ if ($conditions) {
+ foreach ($conditions as $condition) {
+ $filtered = array();
+ $length = count($matches);
+ foreach ($matches as $i => $match) {
+ if (static::matches(array($condition), $match['item'], $i + 1, $length)) {
+ $filtered[] = $match;
+ }
+ }
+ $matches = $filtered;
+ }
+ }
+ $contexts = $matches;
+
+ if (empty($tokens)) {
+ break;
+ }
+ } while (1);
+
+ $r = array();
+
+ foreach ($matches as $match) {
+ if ((!$options['flatten'] || is_array($match['item'])) && !is_int($match['key'])) {
+ $r[] = array($match['key'] => $match['item']);
+ } else {
+ $r[] = $match['item'];
+ }
+ }
+ return $r;
+ }
+
+ /**
+ * Inserts $data into an array as defined by $path.
+ *
+ * @param mixed $list Where to insert into
+ * @param mixed $path A dot-separated string.
+ * @param array $data Data to insert
+ * @return array
+ */
+ public static function insert($list, $path, $data = null) {
+ if (!is_array($path)) {
+ $path = explode('.', $path);
+ }
+ $_list =& $list;
+
+ foreach ($path as $i => $key) {
+ if (is_numeric($key) && intval($key) > 0 || $key === '0') {
+ $key = intval($key);
+ }
+ if ($i === count($path) - 1) {
+ $_list[$key] = $data;
+ } else {
+ if (!isset($_list[$key])) {
+ $_list[$key] = array();
+ }
+ $_list =& $_list[$key];
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Removes an element from a Set or array as defined by $path.
+ *
+ * @param mixed $list From where to remove
+ * @param mixed $path A dot-separated string.
+ * @return array Array with $path removed from its value
+ */
+ public static function remove($list, $path = null) {
+ if (empty($path)) {
+ return $list;
+ }
+ if (!is_array($path)) {
+ $path = explode('.', $path);
+ }
+ $_list =& $list;
+
+ foreach ($path as $i => $key) {
+ if (is_numeric($key) && intval($key) > 0 || $key === '0') {
+ $key = intval($key);
+ }
+ if ($i === count($path) - 1) {
+ unset($_list[$key]);
+ } else {
+ if (!isset($_list[$key])) {
+ return $list;
+ }
+ $_list =& $_list[$key];
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Computes the difference between a Set and an array, two Sets, or two arrays
+ *
+ * @param mixed $val1 First value
+ * @param mixed $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;
+ }
+ $out = array();
+
+ foreach ($val1 as $key => $val) {
+ $exists = array_key_exists($key, $val2);
+
+ if ($exists && $val2[$key] != $val) {
+ $out[$key] = $val;
+ } elseif (!$exists) {
+ $out[$key] = $val;
+ }
+ unset($val2[$key]);
+ }
+
+ foreach ($val2 as $key => $val) {
+ if (!array_key_exists($key, $out)) {
+ $out[$key] = $val;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Determines if one Set or array contains the exact keys and values of another.
+ *
+ * @param array $val1 First value
+ * @param array $val2 Second value
+ * @return boolean true if $val1 contains $val2, false otherwise
+ * @access public
+ */
+ public static function contains($val1, $val2 = null) {
+
+ if (empty($val1) || empty($val2)) {
+ return false;
+ }
+
+ foreach ($val2 as $key => $val) {
+ if (is_numeric($key)) {
+ static::contains($val, $val1);
+ } else {
+ if (!isset($val1[$key]) || $val1[$key] != $val) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Normalizes a string or array list.
+ *
+ * @param mixed $list List to normalize
+ * @param boolean $assoc If true, $list will be converted to an associative array
+ * @param string $sep If $list is a string, it will be split into an array with $sep
+ * @param boolean $trim If true, separated strings will be trimmed
+ * @return array
+ */
+ 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;
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+ return $list;
+ }
+
+ /**
+ * Creates an associative array using a $path1 as the path to build its keys, and optionally
+ * $path2 as path to get the values. If $path2 is not specified, all values will be initialized
+ * to null (useful for Set::merge). You can optionally group the values by what is obtained when
+ * following the path specified in $groupPath.
+ *
+ * @param array $data Array from where to extract keys and values
+ * @param mixed $path1 As an array, or as a dot-separated string.
+ * @param mixed $path2 As an array, or as a dot-separated string.
+ * @param string $groupPath As an array, or as a dot-separated string.
+ * @return array Combined array
+ */
+ public static function combine($data, $path1 = null, $path2 = null, $groupPath = null) {
+ if (empty($data)) {
+ return array();
+ }
+
+ if (is_object($data)) {
+ $data = get_object_vars($data);
+ }
+
+ if (is_array($path1)) {
+ $format = array_shift($path1);
+ $keys = static::format($data, $format, $path1);
+ } else {
+ $keys = static::extract($data, $path1);
+ }
+ $vals = array();
+ if (!empty($path2) && is_array($path2)) {
+ $format = array_shift($path2);
+ $vals = static::format($data, $format, $path2);
+ } elseif (!empty($path2)) {
+ $vals = static::extract($data, $path2);
+ }
+
+ $valCount = count($vals);
+ $count = count($keys);
+ for ($i = $valCount; $i < $count; $i++) {
+ $vals[$i] = null;
+ }
+
+ if ($groupPath != null) {
+ $group = static::extract($data, $groupPath);
+ if (!empty($group)) {
+ $c = count($keys);
+ for ($i = 0; $i < $c; $i++) {
+ if (!isset($group[$i])) {
+ $group[$i] = 0;
+ }
+ if (!isset($out[$group[$i]])) {
+ $out[$group[$i]] = array();
+ }
+ $out[$group[$i]][$keys[$i]] = $vals[$i];
+ }
+ return $out;
+ }
+ }
+ return array_combine($keys, $vals);
+ }
+
+ /**
+ * Converts an object into an array. If $object is no object, reverse
+ * will return the same value.
+ *
+ * @param object $object Object to reverse
+ * @return array
+ */
+ public static function reverse($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::reverse($value);
+ } else {
+ if (isset($value->_name_)) {
+ $new = array_merge($new, static::reverse($value));
+ } else {
+ $new[$key] = static::reverse($value);
+ }
+ }
+ }
+ if (isset($identity)) {
+ $out[$identity] = $new;
+ } else {
+ $out = $new;
+ }
+ } elseif (is_array($object)) {
+ foreach ($object as $key => $value) {
+ $out[$key] = static::reverse($value);
+ }
+ } else {
+ $out = $object;
+ }
+ return $out;
+ }
+
+ /**
+ * Sorts an array by any value, determined by a Set-compatible path
+ *
+ * @param array $data
+ * @param string $path A Set-compatible path to the array value
+ * @param string $dir [optional] asc/desc [default] asc
+ * @return array
+ */
+ public static function sort($data, $path, $dir = 'asc') {
+ $flatten = function($flatten, $results, $key = null) {
+ $stack = array();
+ foreach ((array)$results as $k => $r) {
+ $id = $k;
+ if (!is_null($key)) {
+ $id = $key;
+ }
+ if (is_array($r)) {
+ $stack = array_merge($stack, $flatten($flatten, $r, $id));
+ } else {
+ $stack[] = array('id' => $id, 'value' => $r);
+ }
+ }
+ return $stack;
+ };
+ $extract = static::extract($data, $path);
+ $result = $flatten($flatten, $extract);
+
+ list($keys, $values) = array(
+ static::extract($result, '/id'),
+ static::extract($result, '/value')
+ );
+ if ($dir === 'desc') {
+ $dir = SORT_DESC;
+ } else {
+ $dir = SORT_ASC;
+ }
+
+ array_multisort($values, $dir, $keys, $dir);
+ $sorted = array();
+ $keys = array_unique($keys);
+
+ foreach ($keys as $k) {
+ $sorted[] = $data[$k];
+ }
+ return $sorted;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/Socket.php b/libraries/lithium/util/Socket.php
new file mode 100644
index 0000000..53a0d5b
--- /dev/null
+++ b/libraries/lithium/util/Socket.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\util;
+
+abstract class Socket extends \lithium\core\Object {
+
+ protected $_resource = null;
+
+ public function __construct($config) {
+ $defaults = array(
+ 'persistent' => false,
+ 'protocol' => 'tcp',
+ 'host' => 'localhost',
+ 'login' => 'root',
+ 'password' => '',
+ 'port' => 80,
+ 'timeout' => 30
+ );
+ parent::__construct((array)$config + $defaults);
+ }
+
+ abstract public function open();
+
+ abstract public function close();
+
+ abstract public function eof();
+
+ abstract public function read();
+
+ abstract public function write($data);
+
+ abstract public function timeout($time);
+
+ abstract public function encoding($charset);
+
+ public function __destruct() {
+ $this->close();
+ }
+
+ public function resource() {
+ return $this->_resource;
+ }
+}
\ No newline at end of file
diff --git a/libraries/lithium/util/String.php b/libraries/lithium/util/String.php
new file mode 100644
index 0000000..3510c7b
--- /dev/null
+++ b/libraries/lithium/util/String.php
@@ -0,0 +1,336 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util;
+
+class String {
+
+ /**
+ * Generate a random UUID
+ *
+ * @link http://www.ietf.org/rfc/rfc4122.txt
+ * @return string An RFC 4122-compliant UUID
+ * @todo Fix method dependencies on old-school functions and request data access
+ */
+ 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;
+ }
+ return $result;
+ };
+
+ $node = $val('SERVER_ADDR');
+ $pid = null;
+
+ 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 = $val('HOSTNAME');
+ $host = $host ?: $val('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 = null;
+ }
+ // $node = $node ?: crc32(Configure::read('Security.salt'));
+
+ $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",
+ (int)$timeLow, (int)substr($timeMid, 2) & 0xffff, mt_rand(0, 0xfff) | 0x4000,
+ mt_rand(0, 0x3f) | 0x80, mt_rand(0, 0xff), $pid, $node
+ );
+ }
+
+ /**
+ * Create a hash from string using given method.
+ * Fallback on next available method.
+ *
+ * @param string $string String to hash
+ * @param string $type Method to use (sha1/sha256/md5, or any method supported by the `hash()`
+ * fucntion).
+ * @param string $salt
+ * @return string Hash
+ */
+ function hash($string, $type = null, $salt = null) {
+ $string = $salt . $string;
+
+ switch (true) {
+ case (($type == 'sha1' || $type == null) && 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);
+ default:
+ }
+ return md5($string);
+ }
+
+ /**
+ * Replaces variable placeholders inside a $str with any given $data. Each key in the $data
+ * array corresponds to a variable placeholder name in $str. Example:
+ *
+ * Sample:
+ * {{{
+ * String::insert(
+ * 'My name is {:name} and I am {:age} years old.',
+ * array('name' => 'Bob', 'age' => '65')
+ * );
+ * }}}
+ * Returns: My name is Bob and I am 65 years old.
+ *
+ * Available $options are:
+ * - before: The character or string in front of the name of the variable
+ * placeholder (Defaults to ':')
+ * - after: The character or string after the name of the variable placeholder
+ * (Defaults to null)
+ * - escape: The character or string used to escape the before character / string
+ * (Defaults to '\')
+ * - format: A regex to use for matching variable placeholders. Default is:
+ * `'/(?<!\\)\:%s/'` (Overwrites before, after, breaks escape / clean)
+ * - clean: A boolean or array with instructions for `String::clean()`
+ *
+ * @param string $str A string containing variable placeholders
+ * @param string $data A key => val array where each key stands for a placeholder variable
+ * name to be replaced with val
+ * @param string $options An array of options, see description above
+ * @todo Optimize this
+ * @return string
+ */
+ public static function insert($str, $data, $options = array()) {
+ $defaults = array(
+ 'before' => '{:', 'after' => '}', 'escape' => null, 'format' => null, 'clean' => false
+ );
+ $options += $defaults;
+ $format = $options['format'];
+ $data = (array)$data;
+
+ if ($format == 'regex' || (empty($format) && !empty($options['escape']))) {
+ $format = sprintf(
+ '/(?<!%s)%s%%s%s/',
+ preg_quote($options['escape'], '/'),
+ str_replace('%', '%%', preg_quote($options['before'], '/')),
+ str_replace('%', '%%', preg_quote($options['after'], '/'))
+ );
+ }
+
+ if (empty($format) && strpos($str, '?') === false) {
+ $replace = array();
+
+ foreach ($data as $key => $value) {
+ $replace["{$options['before']}{$key}{$options['after']}"] = $value;
+ }
+ $str = strtr($str, $replace);
+ return $options['clean'] ? static::clean($str, $options) : $str;
+ }
+
+ if (strpos($str, '?') !== false) {
+ $offset = 0;
+ while (($pos = strpos($str, '?', $offset)) !== false) {
+ $val = array_shift($data);
+ $offset = $pos + strlen($val);
+ $str = substr_replace($str, $val, $pos, 1);
+ }
+ return $options['clean'] ? static::clean($str, $options) : $str;
+ }
+
+ foreach ($data as $key => $value) {
+ $hashVal = crc32($key);
+ $key = sprintf($format, preg_quote($key, '/'));
+ $str = preg_replace($key, $hashVal, $str);
+ if (is_object($value)) {
+ try {
+ $value = $value->__toString();
+ } catch (Exception $e) {
+ $value = '';
+ }
+ }
+ if (!is_array($value)) {
+ $str = str_replace($hashVal, $value, $str);
+ }
+ }
+
+ if (!isset($options['format']) && isset($options['before'])) {
+ $str = str_replace($options['escape'] . $options['before'], $options['before'], $str);
+ }
+ return $options['clean'] ? static::clean($str, $options) : $str;
+ }
+
+ /**
+ * Cleans up a `Set::insert()`-formatted string with given $options depending on the 'clean'
+ * key in `$options`. The default method used is 'text' but 'html' is also available. The
+ * goal of this function is to replace all whitespace and uneeded markup around placeholders
+ * that did not get replaced by `Set::insert()`.
+ *
+ * @param string $str
+ * @param string $options
+ * @return string
+ */
+ public static function clean($str, $options = array()) {
+ if (!$options['clean']) {
+ return $str;
+ }
+ $clean = $options['clean'];
+ $clean = ($clean === true) ? array('method' => 'text') : $clean;
+ $clean = (!is_array($clean)) ? array('method' => $options['clean']) : $clean;
+
+ switch ($clean['method']) {
+ case 'html':
+ $clean += array('word' => '[\w,.]+', 'andText' => true, 'replacement' => '');
+ $kleenex = sprintf(
+ '/[\s]*[a-z]+=(")(%s%s%s[\s]*)+\\1/i',
+ preg_quote($options['before'], '/'),
+ $clean['word'],
+ preg_quote($options['after'], '/')
+ );
+ $str = preg_replace($kleenex, $clean['replacement'], $str);
+
+ if ($clean['andText']) {
+ $options['clean'] = array('method' => 'text');
+ $str = static::clean($str, $options);
+ }
+ break;
+ case 'text':
+ $clean += array(
+ 'word' => '[\w,.]+', 'gap' => '[\s]*(?:(?:and|or|,)[\s]*)?', 'replacement' => ''
+ );
+ $before = preg_quote($options['before'], '/');
+ $after = preg_quote($options['after'], '/');
+
+ $kleenex = sprintf(
+ '/(%s%s%s%s|%s%s%s%s|%s%s%s%s%s)/',
+ $before, $clean['word'], $after, $clean['gap'],
+ $clean['gap'], $before, $clean['word'], $after,
+ $clean['gap'], $before, $clean['word'], $after, $clean['gap']
+ );
+ $str = preg_replace($kleenex, $clean['replacement'], $str);
+ break;
+ }
+ return $str;
+ }
+ /**
+ * Extract a part of a string based on a regular expression `$regex`
+ *
+ * @param string $regex The regular expression to use
+ * @param string $str The string to run the extraction on
+ * @param int $index The number of the part to return based on the regex
+ * @return mixed
+ */
+ static function extract($regex, $str, $index = 0) {
+ if (!preg_match($regex, $str, $match)) {
+ return false;
+ }
+ return isset($match[$index]) ? $match[$index] : null;
+ }
+
+ /**
+ * Tokenizes a string using `$separator`, ignoring any instance of `$separator` that appears
+ * between `$leftBound` and `$rightBound`.
+ *
+ * @param string $data The data to tokenize
+ * @param string $separator The token to split the data on
+ * @return array
+ */
+ public static function tokenize($data, $separator = ',', $leftBound = '(', $rightBound = ')') {
+ if (empty($data) || is_array($data)) {
+ return $data;
+ }
+
+ $depth = 0;
+ $offset = 0;
+ $buffer = '';
+ $results = array();
+ $length = strlen($data);
+ $open = false;
+
+ while ($offset <= $length) {
+ $tmpOffset = -1;
+ $offsets = array(
+ strpos($data, $separator, $offset),
+ strpos($data, $leftBound, $offset),
+ strpos($data, $rightBound, $offset)
+ );
+ for ($i = 0; $i < 3; $i++) {
+ if ($offsets[$i] !== false && ($offsets[$i] < $tmpOffset || $tmpOffset == -1)) {
+ $tmpOffset = $offsets[$i];
+ }
+ }
+ if ($tmpOffset !== -1) {
+ $buffer .= substr($data, $offset, ($tmpOffset - $offset));
+ if ($data{$tmpOffset} == $separator && $depth == 0) {
+ $results[] = $buffer;
+ $buffer = '';
+ } else {
+ $buffer .= $data{$tmpOffset};
+ }
+ if ($leftBound != $rightBound) {
+ if ($data{$tmpOffset} == $leftBound) {
+ $depth++;
+ }
+ if ($data{$tmpOffset} == $rightBound) {
+ $depth--;
+ }
+ } else {
+ if ($data{$tmpOffset} == $leftBound) {
+ if (!$open) {
+ $depth++;
+ $open = true;
+ } else {
+ $depth--;
+ $open = false;
+ }
+ }
+ }
+ $offset = ++$tmpOffset;
+ } else {
+ $results[] = $buffer . substr($data, $offset);
+ $offset = $length + 1;
+ }
+ }
+
+ if (empty($results) && !empty($buffer)) {
+ $results[] = $buffer;
+ }
+ return empty($results) ? array() : array_map('trim', $results);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/Validator.php b/libraries/lithium/util/Validator.php
new file mode 100644
index 0000000..8d613e4
--- /dev/null
+++ b/libraries/lithium/util/Validator.php
@@ -0,0 +1,831 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (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;
+
+class Validator extends \lithium\core\StaticObject {
+
+ /**
+ * An array of validation rules. May contain a single regular expression, an array of regular
+ * expressions (where the array keys define various possible 'formats' of the same rule), or a
+ * closure which accepts a value to be validated, and an array of options, and returns a
+ * boolean value, indicating whether the validation succeeded or failed.
+ *
+ * @var array
+ * @see lithium\util\Validator::add()
+ * @see lithium\util\Validator::rule()
+ */
+ protected static $_rules = array();
+
+ /**
+ * Two-dimensional array of closures which are invoked on a value before a validation is
+ * performed. The array keys of the first level are the validation to be performed, i.e.
+ * `'alphaNumeric'` for `Validator::isAlphaNumeric()`, and the second level is simply a
+ * numeric array, indicating the order in which the closures should be executed.
+ *
+ * @var array
+ * @see lithium\util\Validator::filter()
+ * @see lithium\util\Validator::rule()
+ * @see lithium\util\Validator::$_postFilters
+ */
+ protected static $_preFilters = array();
+
+ /**
+ * Two-dimensional array of closures which are invoked on a value after a validation succeeds.
+ * See corresponding `$_preFilters` array for more information. Unlike pre-filters, these
+ * post-filters provide an extra layer of validation if the primary rule succeeds. Often these
+ * filters are used for more in-depth checking, i.e. validating that the host name of an email
+ * address resolves to a valid IP, should a simple regex check succeed.
+ *
+ * @var array
+ * @see lithium\util\Validator::filter()
+ * @see lithium\util\Validator::rule()
+ * @see lithium\util\Validator::$_preFilters
+ */
+ protected static $_postFilters = array();
+
+ protected static $_options = array(
+ 'ip' => array('contains' => false),
+ 'defaults' => array('contains' => true)
+ );
+
+ /**
+ * Initializes the list of default validation rules.
+ *
+ * @return void
+ */
+ public static function __init() {
+ $alnum = '[A-Fa-f0-9]';
+
+ static::$_rules = array(
+ 'alphaNumeric' => '/^[\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]+$/mu',
+ 'blank' => '/[^\\s]/',
+ 'creditCard' => array(
+ 'amex' => '/^3[4|7]\\d{13}$/',
+ 'bankcard' => '/^56(10\\d\\d|022[1-5])\\d{10}$/',
+ 'diners' => '/^(?:3(0[0-5]|[68]\\d)\\d{11})|(?:5[1-5]\\d{14})$/',
+ 'disc' => '/^(?:6011|650\\d)\\d{12}$/',
+ 'electron' => '/^(?:417500|4917\\d{2}|4913\\d{2})\\d{10}$/',
+ 'enroute' => '/^2(?:014|149)\\d{11}$/',
+ 'jcb' => '/^(3\\d{4}|2100|1800)\\d{11}$/',
+ 'maestro' => '/^(?:5020|6\\d{3})\\d{12}$/',
+ 'mc' => '/^5[1-5]\\d{14}$/',
+ 'solo' => '/^(6334[5-9][0-9]|6767[0-9]{2})\\d{10}(\\d{2,3})?$/',
+ 'switch' => '/^(?:49(03(0[2-9]|3[5-9])|11(0[1-2]|7[4-9]|8[1-2])|36[0-9]{2})' .
+ '\\d{10}(\\d{2,3})?)|(?:564182\\d{10}(\\d{2,3})?)|(6(3(33[0-4]' .
+ '[0-9])|759[0-9]{2})\\d{10}(\\d{2,3})?)$/',
+ '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})$/'
+ ),
+ 'date' => array(
+ 'dmy' => '%^(?:(?:31(\\/|-|\\.|\\x20)(?:0?[13578]|1[02]))\\1|(?:(?:29|30)' .
+ '(\\/|-|\\.|\\x20)(?:0?[1,3-9]|1[0-2])\\2))(?:(?:1[6-9]|[2-9]\\d)?' .
+ '\\d{2})$|^(?:29(\\/|-|\\.|\\x20)0?2\\3(?:(?:(?:1[6-9]|[2-9]\\d)?' .
+ '(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])' .
+ '00))))$|^(?:0?[1-9]|1\\d|2[0-8])(\\/|-|\\.|\\x20)(?:(?:0?[1-9])|' .
+ '(?:1[0-2]))\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$%',
+ 'mdy' => '%^(?:(?:(?:0?[13578]|1[02])(\\/|-|\\.|\\x20)31)\\1|(?:(?:0?[13-9]|' .
+ '1[0-2])(\\/|-|\\.|\\x20)(?:29|30)\\2))(?:(?:1[6-9]|[2-9]\\d)?\\d' .
+ '{2})$|^(?:0?2(\\/|-|\\.|\\x20)29\\3(?:(?:(?:1[6-9]|[2-9]\\d)?' .
+ '(?:0[48]|[2468][048]|[13579][26])|(?:(?:16|[2468][048]|[3579][26])' .
+ '00))))$|^(?:(?:0?[1-9])|(?:1[0-2]))(\\/|-|\\.|\\x20)(?:0?[1-9]|1' .
+ '\\d|2[0-8])\\4(?:(?:1[6-9]|[2-9]\\d)?\\d{2})$%',
+ 'ymd' => '%^(?:(?:(?:(?:(?:1[6-9]|[2-9]\\d)?(?:0[48]|[2468][048]|[13579]' .
+ '[26])|(?:(?:16|[2468][048]|[3579][26])00)))(\\/|-|\\.|\\x20)' .
+ '(?:0?2\\1(?:29)))|(?:(?:(?:1[6-9]|[2-9]\\d)?\\d{2})(\\/|-|\\.|' .
+ '\\x20)(?:(?:(?:0?[13578]|1[02])\\2(?:31))|(?:(?:0?[1,3-9]|1[0-2])' .
+ '\\2(29|30))|(?:(?:0?[1-9])|(?:1[0-2]))\\2(?:0?[1-9]|1\\d|2[0-8]' .
+ '))))$%',
+ 'dMy' => '/^((31(?!\\ (Feb(ruary)?|Apr(il)?|June?|(Sep(?=\\b|t)t?|Nov)' .
+ '(ember)?)))|((30|29)(?!\\ Feb(ruary)?))|(29(?=\\ Feb(ruary)?\\ ' .
+ '(((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468]' .
+ '[048]|[3579][26])00)))))|(0?[1-9])|1\\d|2[0-8])\\ (Jan(uary)?|' .
+ 'Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|Aug(ust)?|' .
+ 'Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)\\ ((1[6-9]|[2-9]' .
+ '\\d)\\d{2})$/',
+ 'Mdy' => '/^(?:(((Jan(uary)?|Ma(r(ch)?|y)|Jul(y)?|Aug(ust)?|Oct(ober)?' .
+ '|Dec(ember)?)\\ 31)|((Jan(uary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)' .
+ '|(ne?))|Aug(ust)?|Oct(ober)?|(Sept|Nov|Dec)(ember)?)\\ (0?[1-9]' .
+ '|([12]\\d)|30))|(Feb(ruary)?\\ (0?[1-9]|1\\d|2[0-8]|(29(?=,?\\ ' .
+ '((1[6-9]|[2-9]\\d)(0[48]|[2468][048]|[13579][26])|((16|[2468]' .
+ '[048]|[3579][26])00)))))))\\,?\\ ((1[6-9]|[2-9]\\d)\\d{2}))$/',
+ 'My' => '%^(Jan(uary)?|Feb(ruary)?|Ma(r(ch)?|y)|Apr(il)?|Ju((ly?)|(ne?))|' .
+ 'Aug(ust)?|Oct(ober)?|(Sep(?=\\b|t)t?|Nov|Dec)(ember)?)[ /]((1[6-9]' .
+ '|[2-9]\\d)\\d{2})$%',
+ 'my' => '%^(((0[123456789]|10|11|12)([- /.])(([1][9][0-9][0-9])|([2][0-9]' .
+ '[0-9][0-9]))))$%'
+ ),
+ 'hostname' => '(?:[a-z0-9][-a-z0-9]*\.)*(?:[a-z0-9][-a-z0-9]{0,62})\.' .
+ '(?:(?:[a-z]{2}\.)?[a-z]{2,4}|museum|travel)',
+ 'ip' => '(?:(?:25[0-5]|2[0-4][0-9]|(?:(?:1[0-9])?|[1-9]?)[0-9])\.){3}' .
+ '(?:25[0-5]|2[0-4][0-9]|(?:(?:1[0-9])?|[1-9]?)[0-9])',
+ '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'
+ ),
+ 'notEmpty' => '/[^\s]+/m',
+ 'phone' => array(
+ 'us' => '/^(?:\+?1)?[-. ]?\\(?[2-9][0-8][0-9]\\)?[-. ]?[2-9][0-9]{2}[-. ]' .
+ '?[0-9]{4}$/',
+ ),
+ 'postalCode' => array(
+ 'uk' => '/\\A\\b[A-Z]{1,2}[0-9][A-Z0-9]? [0-9][ABD-HJLNP-UW-Z]{2}\\b\\z/i',
+ 'ca' => '/\\A\\b[ABCEGHJKLMNPRSTVXY][0-9][A-Z] [0-9][A-Z][0-9]\\b\\z/i',
+ 'it' => '/^[0-9]{5}$/i',
+ 'de' => '/^[0-9]{5}$/i',
+ 'be' => '/^[1-9]{1}[0-9]{3}$/i',
+ 'us' => '/\\A\\b[0-9]{5}(?:-[0-9]{4})?\\b\\z/i'
+ ),
+ 'regex' => '/^\/(.+)\/[gimsxu]*$/',
+ 'ssn' => array(
+ 'dk' => '/\\A\\b[0-9]{6}-[0-9]{4}\\b\\z/i',
+ 'nl' => '/\\A\\b[0-9]{9}\\b\\z/i',
+ 'us' => '/\\A\\b[0-9]{3}-[0-9]{2}-[0-9]{4}\\b\\z/i'
+ ),
+ 'time' => '%^((0?[1-9]|1[012])(:[0-5]\d){0,2}([AP]M|[ap]m))$|^([01]\d|2[0-3])' .
+ '(:[0-5]\d){0,2}$%',
+ 'boolean' => function($value) {
+ return in_array($value, array(0, 1, '0', '1', true, false), true);
+ },
+ 'decimal' => function($value, $format = null, $options = array()) {
+ $defaults = array('precision' => null);
+ $options += $defaults;
+
+ $precision = '+(?:[eE][-+]?[0-9]+)?';
+ $precision = $options['precision'] ? '{' . $options['precision'] . '}' : $precision;
+ return (bool)preg_match("/^[-+]?[0-9]*\\.{1}[0-9]{$precision}$/", (string)$value);
+ },
+ 'inList' => function($value, $format, $options) {
+ $options += array('list' => array());
+ return in_array($value, $options['list']);
+ },
+ 'lengthBetween' => function($value, $format, $options) {
+ $length = strlen($value);
+ $options += array('min' => 1, 'max' => 255);
+ return ($length >= $options['min'] && $length <= $options['max']);
+ },
+ 'luhn' => function($value) {
+ if (empty($value) || !is_string($value)) {
+ return false;
+ }
+ $sum = 0;
+ $length = strlen($value);
+
+ for ($position = 1 - ($length % 2); $position < $length; $position += 2) {
+ $sum += $value[$position];
+ }
+ for ($position = ($length % 2); $position < $length; $position += 2) {
+ $number = $value[$position] * 2;
+ $sum += ($number < 10) ? $number : $number - 9;
+ }
+ return ($sum % 10 == 0);
+ },
+ 'numeric' => function($value) {
+ return is_numeric($value);
+ },
+ 'uuid' => "/{$alnum}{8}-{$alnum}{4}-{$alnum}{4}-{$alnum}{4}-{$alnum}{12}/"
+ );
+
+ static::$_rules['email'] = '/^[a-z0-9!#$%&\'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+\/=?^_`';
+ static::$_rules['email'] .= '{|}~-]+)*@' . static::$_rules['hostname'] . '$/i';
+
+ $urlChars = '([' . preg_quote('!"$&\'()*+,-.@_:;=') . '\/0-9a-z]|(%[0-9a-f]{2}))';
+ $url = '/^(?:(?:https?|ftps?|file|news|gopher):\/\/)__strict__';
+ $url .= '(?:' . static::$_rules['ip'] . '|' . static::$_rules['hostname'] . ')';
+ $url .= '(?::[1-9][0-9]{0,3})?(?:\/?|\/' . $urlChars . '*)?(?:\?' . $urlChars . '*)?';
+ $url .= '(?:#' . $urlChars . '*)?$/i';
+
+ static::$_rules['url'] = array(
+ 'strict' => str_replace('__strict__', '', $url),
+ 'loose' => str_replace('__strict__', '?', $url)
+ );
+
+ $emptyCheck = function($value) {
+ if (empty($value) && $value != '0') {
+ return false;
+ }
+ };
+ static::$_preFilters['alphaNumeric'] = array($emptyCheck);
+ static::$_preFilters['notEmpty'] = array($emptyCheck);
+
+ static::$_preFilters['creditCard'] = array(function($value, $format, $options) {
+ $value = str_replace(array('-', ' '), '', $value);
+ return (strlen($value) < 13) ? false : $value;
+ });
+
+ static::$_postFilters['creditCard'] = array(function($value, $format, $options) {
+ $options += array('deep' => false);
+ return $options['deep'] ? Validator::isLuhn($value) : true;
+ });
+ $host = static::$_rules['hostname'];
+
+ static::$_postFilters['email'] = array(function($value, $format, $options) use ($host) {
+ $options += array('deep' => false);
+
+ if (!$options['deep']) {
+ return true;
+ }
+
+ if (preg_match('/@(' . $host . ')$/i', $value, $regs)) {
+ return is_array(gethostbynamel($regs[1]));
+ }
+ });
+ }
+
+ /**
+ * Maps method calls to validation rule names. For example, a validation rule that would
+ * normally be called as `Validator::rule('email', 'foo@bar.com')` can also be called as
+ * `Validator::isEmail('foo@bar.com')`.
+ *
+ * @param string $method The name of the method called, i.e. `'isEmail'` or `'isCreditCard'`.
+ * @param array $args
+ * @return boolean
+ */
+ public static function __callStatic($method, $args = array()) {
+ if (!isset($args[0])) {
+ return false;
+ }
+ $args += array(1 => 'any', 2 => array());
+ $rule = preg_replace("/^is([A-Z][A-Za-z0-9]+)$/", '$1', $method);
+ $rule[0] = strtolower($rule[0]);
+
+ return static::rule($rule, $args[0], $args[1], $args[2]);
+ }
+
+ /**
+ * 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 string $values An array of key/value pairs, where the values are to be checked.
+ * @param string $rules
+ * @return mixed When all validation rules pass
+ * @todo Bring over validation loop from Model, determine formats/options, implement.
+ */
+ public static function check($values, $rules, $options = array()) {
+ }
+
+ /**
+ * Adds to or replaces built-in validation rules specified in `Validator::$_rules`. Any new
+ * validation rules created are automatically callable as validation methods.
+ *
+ * For example:
+ * {{{
+ * Validator::add('zeroToNine', '/^[0-9]$/');
+ * $isValid = Validator::isZeroToNine("5"); // true
+ * $isValid = Validator::isZeroToNine("20"); // false
+ * }}}
+ *
+ * Alternatively, the first parameter may be an array of rules expressed as key/value pairs,
+ * as in the following:
+ * {{{
+ * Validator::add(array(
+ * 'zeroToNine' => '/^[0-9]$/',
+ * 'tenToNineteen' => '/^1[0-9]$/',
+ * ));
+ * }}}
+ *
+ * @param mixed $name The name of the validation rule (string), or an array of key/value pairs
+ * of names and rules.
+ * @param string $rule If $name is a string, this should be a string regular expression, or a
+ * closure that returns a boolean indicating success. Should be left blank if
+ * `$name` is an array.
+ * @param array $options The default options for validating this rule. An option which applies
+ * to all regular expression rules is `'contains'` which, if set to true, allows
+ * validated values to simply _contain_ a match to a rule, rather than exactly
+ * matching it in whole.
+ * @return void
+ */
+ public static function add($name, $rule = null, $options = array()) {
+ if (!is_array($name)) {
+ $name = array($name => $rule);
+ }
+ static::$_rules = Set::merge(static::$_rules, $name);
+
+ if (!empty($options)) {
+ $options = array_combine(array_keys($name), array_fill(0, count($name), $options));
+ static::$_options = Set::merge(static::$_options, $options);
+ }
+ }
+
+ /**
+ * Adds, removes, or gets pre- or post-filters which are executed on a value before a validation
+ * is attempted, and after a validation succeeds, respectively. Each pre-filter (closure)
+ * transforms the value before it is passed on to the validation rule for checking. Each
+ * post-filter takes a value that has already passed validation, and performs additional
+ * validation on it.
+ *
+ * @param string $type Specifies which type of filter to work with, either `'before'` for
+ * pre-filters, or `'after'` for post-filters.
+ * @param string $rule The name of the rule for which this filter will be added. For example,
+ * to add a filter for `Validator::isAlphaNumeric()`, use `'alphaNumeric'`.
+ * @param mixed $filter A closure which should accept 3 parameters:
+ * - `$value`: The value to be validated.
+ * - `$format`: The specific format of the validation rule.
+ * - `$options`: An array of options specifying how the validation will be performed.
+ * For pre-filters, `$filter` should return the newly-transformed value, which will be
+ * checked against the validation rule. Null return values are ignored. True values
+ * automatically succeed, and false values automatically fail. For post-filters,
+ * `$filter` should return a boolean value, indicating whether the filter's additional
+ * validation checking succeeded. If `$filter` is set to `false`, all filters assigned
+ * to `$rule` (either pre or post, depending on `$type`) are removed.
+ * @return mixed If filter is null, returns an array containing all the filters assigned to
+ * `$rule`. Otherwise, returns null.
+ */
+ public static function filter($type, $rule, $filter = null) {
+ $types = array('before' => '_preFilters', 'after' => '_postFilters');
+ if (!isset($types[$type])) {
+ throw new InvalidArgumentException('Invalid filter type ' . $type);
+ }
+ $type = $types[$type];
+
+ if (!isset(static::${$type}[$rule])) {
+ static::${$type}[$rule] = array();
+ }
+ if (is_null($filter)) {
+ return static::${$type}[$rule];
+ }
+ if ($filter === false) {
+ static::${$type}[$rule] = array();
+ return;
+ }
+ static::${$type}[$rule][] = $filter;
+ }
+
+ /**
+ * Checks a single value against a single validation rule in one or more formats.
+ *
+ * @param string $rule
+ * @param mixed $value
+ * @param string $format
+ * @param string $options
+ * @return boolean
+ * @todo Write tests for pre- and post-filtering
+ */
+ public static function rule($rule, $value, $format = 'any', $options = array()) {
+ if (!isset(static::$_rules[$rule])) {
+ throw new InvalidArgumentException("Rule '{$rule}' is not a validation rule");
+ }
+ $defaults = isset(static::$_options[$rule]) ? static::$_options[$rule] : array();
+ $options += $defaults + static::$_options['defaults'];
+ $result = static::_filters('before', $rule, compact('value', 'format', 'options'));
+
+ if ($result === true || $result === false) {
+ return $result;
+ }
+ $value = is_null($result) ? $value : $result;
+
+ $ruleCheck = static::$_rules[$rule];
+ $ruleCheck = is_array($ruleCheck) ? $ruleCheck : array($ruleCheck);
+
+ if (!$options['contains'] && !empty($ruleCheck)) {
+ $append = function($item) { return is_string($item) ? '/^' . $item . '$/' : $item; };
+ $ruleCheck = array_map($append, $ruleCheck);
+ }
+
+ if (in_array($format, array(null, 'all', 'any'))) {
+ $formats = array_keys($ruleCheck);
+ $all = ($format == 'all');
+ } else {
+ $formats = (array)$format;
+ $all = true;
+ }
+
+ if (static::_checkFormats($ruleCheck, $formats, $value, $all, $options)) {
+ return (bool)static::_filters('after', $rule, compact('value', 'format', 'options'));
+ }
+ return false;
+ }
+
+ /**
+ * Runs pre- or post-filters for a given rule and returns the result.
+ *
+ * If a pre-filter returns true or false, the validation immediately succeeds. If a pre-filter
+ * returns null, validation continues. If a pre-filter returns any other value, the value to be
+ * validated is modified, and all subsequent filters and validation rules will run against this
+ * new value.
+ *
+ * If a post-filter returns any true value, validation succeeds, or continues to the next
+ * filter. If a post-filter returns any false value, validation immediately fails.
+ *
+ * Both pre- and post-filters take the same 3 parameters:
+ * - `$value`: The value to be validated.
+ * - `$format`: The format or list of formats against which this rule is being validated,
+ * or null, if the rule is not format-dependent.
+ * - `$options`: Any other options associated with the rule.
+ *
+ * @param string $type Either 'before' or 'after', that indicate which filters to run.
+ * @param string $rule The name of the rule to run the filters for.
+ * @param string $params An array containing `'value'`, `'format'` and `'options'` keys (in
+ * that order), corresponding to the parameters required by the filters.
+ * @return void
+ */
+ protected static function _filters($type, $rule, $params) {
+ $types = array('before' => '_preFilters', 'after' => '_postFilters');
+ $var = $types[$type];
+
+ if (!isset(static::${$var}[$rule])) {
+ return ($type == 'after') ? true : null;
+ }
+ list($value, $format, $options) = array_values($params);
+
+ foreach (static::${$var}[$rule] as $filter) {
+ $result = $filter($value, $format, $options);
+
+ if ($type == 'before') {
+ if ($result === true || $result === false) {
+ return $result;
+ }
+ $value = is_null($result) ? $value : $result;
+ } else {
+ if (!$result) {
+ return false;
+ }
+ }
+ }
+ return ($type == 'before') ? $value : true;
+ }
+
+ /**
+ * Perform validation checks against a value using an array of all possible formats for a rule,
+ * and an array specifying which formats within the rule to use.
+ *
+ * @param array $rules All available rules.
+ * @param array $formats The list of rules to check against.
+ * @param mixed $value The value to perform validation on.
+ * @param boolean $all Whether all rule formats should be validated against. If true, only
+ * return successfully if _all_ formats validate, otherwise, returns true if
+ * _any_ validates.
+ * @param array $options Validation options to be passed to rules defined as closures.
+ * @return boolean Returns true if the rule validation succeeded, otherwise false.
+ * @todo Add exception handling
+ */
+ protected static function _checkFormats($rules, $formats, $value, $all, $options) {
+ $success = false;
+
+ foreach ($formats as $name) {
+ if (!isset($rules[$name])) {
+ // throw some kind of error here
+ continue;
+ }
+ $check = $rules[$name];
+
+ $regexPassed = (is_string($check) && preg_match($check, $value));
+ $closurePassed = (is_object($check) && $check($value, $name, $options));
+
+ if (!$all && ($regexPassed || $closurePassed)) {
+ return true;
+ }
+ if ($all && (!$regexPassed && !$closurePassed)) {
+ return false;
+ }
+ }
+ return $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.
+ *
+ * @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
+ * @see lithium\util\Validator::isLuhn()
+ */
+ // public static function isCreditCard($value, $format = 'fast', $deep = false) {}
+
+ /**
+ * Used to compare 2 numeric values.
+ *
+ * @param mixed $value If string is passed for a string must also be passed for $value2
+ * used as an array it must be passed as
+ * {{{array('check1' => value, 'operator' => 'value', 'check2' => value)}}}
+ * @param string $operator Can be either a word or operand
+ * - is greater >, is less <, greater or equal >=
+ * - less or equal <=, is less <, equal to ==, not equal !=
+ * @param integer $value2 only needed if $value1 is a string
+ * @return boolean Success
+ */
+ public static function compare($value, $operator = null, $value2 = null) {
+ if (is_array($value)) {
+ extract($value, EXTR_OVERWRITE);
+ }
+ $replace = array(' ', "\t", "\n", "\r", "\0", "\x0B");
+ $operator = str_replace($replace, '', strtolower($operator));
+
+ $values = array(
+ '>' => ($value1 > $value2),
+ '<' => ($value1 < $value2),
+ '>=' => ($value1 >= $value2),
+ '<=' => ($value1 <= $value2),
+ '==' => ($value1 == $value2),
+ '!=' => ($value1 != $value2),
+ '===' => ($value1 === $value2)
+ );
+
+ if (array_key_exists($operator, $values)) {
+ return $values[$operator];
+ }
+ return 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 whether the length of a string is greater or equal to a minimal length.
+ *
+ * @param string $value The string to test
+ * @param integer $min The minimal string length
+ * @return boolean Success
+ */
+ public static function hasMinLength($value, $min) {
+ return (strlen($value) >= $min);
+ }
+
+ /**
+ * Checks whether the length of a string is smaller or equal to a maximal length..
+ *
+ * @param string $value The string to test
+ * @param integer $max The maximal string length
+ * @return boolean Success
+ */
+ public static function hasMaxLength($value, $max) {
+ return (strlen($value) <= $max);
+ }
+
+ /**
+ * Checks that a value is a monetary amount.
+ *
+ * @param string $value Value to check
+ * @param string $symbolPosition Where symbol is located (left/right)
+ * @return boolean Success
+ */
+ public static function isMoney($value, $format = 'left') {
+ return static::_rule($value, __METHOD__, $format);
+ }
+
+ /**
+ * Validate a multiple select.
+ *
+ * @param mixed $value Value to check
+ * @param mixed $options Options for the check.
+ * Valid options
+ * in => provide a list of choices that selections must be made from
+ * max => maximun number of non-zero choices that can be made
+ * min => minimum number of non-zero choices that can be made
+ * @return boolean Success
+ */
+ public static function multiple($value, $options = array()) {
+ $defaults = array('in' => null, 'max' => null, 'min' => null);
+ $options += $defaults;
+ $value = array_filter((array)$value);
+
+ if (empty($value)) {
+ return false;
+ }
+ if ($options['max'] && sizeof($value) > $options['max']) {
+ return false;
+ }
+ if ($options['min'] && sizeof($value) < $options['min']) {
+ return false;
+ }
+ if ($options['in'] && is_array($options['in'])) {
+ foreach ($value as $val) {
+ if (!in_array($val, $options['in'])) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ /**
+ * 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) {
+ if (!is_numeric($value)) {
+ return false;
+ }
+ if (isset($lower) && isset($upper)) {
+ return ($value > $lower && $value < $upper);
+ }
+ return is_finite($value);
+ }
+
+ /**
+ * 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/audit/Debugger.php b/libraries/lithium/util/audit/Debugger.php
new file mode 100644
index 0000000..284391a
--- /dev/null
+++ b/libraries/lithium/util/audit/Debugger.php
@@ -0,0 +1,100 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util\audit;
+
+use \lithium\util\String;
+
+class Debugger extends \lithium\core\Object {
+
+ /**
+ * Outputs a stack trace based on the supplied options.
+ *
+ * @param array $options Format for outputting stack trace
+ * @return string Formatted stack trace
+ */
+ public static function trace($options = array()) {
+ $defaults = array(
+ 'depth' => 999,
+ 'format' => null,
+ 'args' => false,
+ 'start' => 0,
+ 'scope' => array(),
+ 'trace' => array(),
+ 'includeScope' => true
+ );
+ $options += $defaults;
+
+ $backtrace = $options['trace'] ?: debug_backtrace();
+ $scope = $options['scope'];
+ $count = count($backtrace);
+ $back = array();
+ $traceDefault = array(
+ 'line' => '??', 'file' => '[internal]', 'class' => null, 'function' => '[main]'
+ );
+
+ for ($i = $options['start']; $i < $count && $i < $options['depth']; $i++) {
+ $trace = array_merge(array('file' => '[internal]', 'line' => '??'), $backtrace[$i]);
+ $function = '[main]';
+
+ if (isset($backtrace[$i + 1])) {
+ $next = $backtrace[$i + 1] + $traceDefault;
+ $function = $next['function'];
+
+ if (!empty($next['class'])) {
+ $function = $next['class'] . '::' . $function . '(';
+ if ($options['args'] && isset($next['args'])) {
+ $args = array_map(array('static', 'export'), $next['args']);
+ $function .= join(', ', $args);
+ }
+ $function .= ')';
+ }
+ }
+
+ if (in_array($function, array('call_user_func_array', 'trigger_error'))) {
+ continue;
+ }
+ $trace['functionRef'] = $function;
+
+ if ($options['format'] == 'points' && $trace['file'] != '[internal]') {
+ $back[] = array('file' => $trace['file'], 'line' => $trace['line']);
+ } elseif (is_string($options['format']) && $options['format'] != 'array') {
+ $back[] = String::insert($options['format'], array_map(
+ function($data) { return is_object($data) ? get_class($data) : $data; },
+ $trace
+ ));
+ } elseif (empty($options['format'])) {
+ $back[] = $function . ' - ' . $trace['file'] . ', line ' . $trace['line'];
+ } else {
+ $back[] = $trace;
+ }
+
+ if (!empty($scope) && array_intersect_assoc($scope, $trace) == $scope) {
+ if (!$options['includeScope']) {
+ $back = array_slice($back, 0, count($back) - 1);
+ }
+ break;
+ }
+ }
+
+ if ($options['format'] == 'array' || $options['format'] == 'points') {
+ return $back;
+ }
+ return join("\n", $back);
+ }
+
+ public static function export($var) {
+ return var_export($var, true);
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/audit/Logger.php b/libraries/lithium/util/audit/Logger.php
new file mode 100644
index 0000000..473c3d3
--- /dev/null
+++ b/libraries/lithium/util/audit/Logger.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace lithium\util\audit;
+
+use \lithium\util\String;
+use \lithium\core\Libraries;
+use \lithium\util\Collection;
+
+class Logger extends \lithium\core\Adaptable {
+
+ /**
+ * Stores configurations for cache adapters
+ *
+ * @var object Collection of logger configurations
+ */
+ protected static $_configurations = null;
+
+ /**
+ * Writes $message to the log specified by the $name
+ * configuration.
+ *
+ * @param string $name Configuration to be used for writing
+ * @param string $message Message to be written
+ * @return boolean True on successful write, false otherwise
+ */
+ public static function write($type, $message) {
+ $settings = static::config();
+
+ if (!isset($settings[$type]) || !$settings->count()) {
+ return false;
+ }
+
+ $methods = array($type => static::adapter($type)->write($type, $message));
+ $result = false;
+
+ foreach ($methods as $name => $method) {
+ $params = compact('type', 'message');
+ $filters = $settings[$name]['filters'];
+ $result = $result || static::_filter('write', $params, $method, $filters);
+ }
+ return $result;
+ }
+
+ /**
+ * Returns adapter for given named configuration
+ *
+ * @param string $name Cache configuration name
+ * @return object Adapter for named configuration
+ */
+ public static function adapter($name) {
+ return static::_adapter('adapters.util.audit.logger', $name);
+ }
+}
+
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/audit/logger/adapters/File.php b/libraries/lithium/util/audit/logger/adapters/File.php
new file mode 100644
index 0000000..08c22d6
--- /dev/null
+++ b/libraries/lithium/util/audit/logger/adapters/File.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace lithium\util\audit\logger\adapters;
+
+use \SplFileInfo;
+use \DirectoryIterator;
+
+class File extends \lithium\core\Object {
+
+ /**
+ * Class constructor
+ *
+ * @return void
+ */
+ public function __construct($config = array()) {
+ $defaults = array('path' => LITHIUM_APP_PATH . '/tmp/logs');
+ parent::__construct($config + $defaults);
+ }
+
+ /**
+ * Appends $data to file $type.
+ *
+ * @param string $type
+ * @param string $message
+ * @return boolean True on successful write, false otherwise
+ */
+ public function write($type, $message) {
+ $path = $this->_config['path'];
+
+ return function($self, $params, $chain) use (&$path) {
+ extract($params);
+ $message .= "\n";
+ return file_put_contents("$path/$type.log", $message, FILE_APPEND);
+ };
+
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/collection/Filters.php b/libraries/lithium/util/collection/Filters.php
new file mode 100644
index 0000000..f000fdf
--- /dev/null
+++ b/libraries/lithium/util/collection/Filters.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util\collection;
+
+class Filters extends \lithium\util\Collection {
+
+ protected $_autoConfig = array('items', 'class', 'method');
+
+ protected $_class = null;
+
+ protected $_method = null;
+
+ /**
+ * Provides short-hand convenience syntax for filter chaining.
+ *
+ * @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);
+ }
+
+ /**
+ * Gets the method name associated with this filter chain. This is the method being filtered.
+ *
+ * @return void
+ */
+ public function method($full = false) {
+ return $full ? $this->_class . '::' . $this->_method : $this->_method;
+ }
+}
+
+?>
diff --git a/libraries/lithium/util/reflection/Coverage.php b/libraries/lithium/util/reflection/Coverage.php
new file mode 100644
index 0000000..f920c8b
--- /dev/null
+++ b/libraries/lithium/util/reflection/Coverage.php
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util\reflection;
+
+use \lithium\util\reflection\Parser;
+
+/**
+ * Runs documentation coverage analysis for classes, properties and methods.
+ */
+class Coverage extends \lithium\core\Object {
+
+ function check($class) {
+
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/reflection/Docblock.php b/libraries/lithium/util/reflection/Docblock.php
new file mode 100644
index 0000000..ec800d2
--- /dev/null
+++ b/libraries/lithium/util/reflection/Docblock.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util\reflection;
+
+class Docblock extends \lithium\core\StaticObject {
+
+ /**
+ * undocumented function
+ *
+ * @param string $description
+ * @return array
+ * @todo Implement text
+ */
+ public static function comment($description) {
+ $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);
+ ini_set('xdebug.var_display_max_data', 2048);
+
+ foreach (array_values(array_slice($tagContents, 1)) as $i => $desc) {
+ $description = trim(str_replace("@{$tagNames[1][$i]} {$desc}", '', $description));
+ $tag = $tagNames[1][$i];
+
+ 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']);
+ }
+ $text = '';
+
+ if (strpos($description, "\n\n")) {
+ list($description, $text) = explode("\n\n", $description, 2);
+ }
+ $text = trim($text);
+ $description = trim($description);
+ return compact('description', 'text', 'tags');
+ }
+
+ /**
+ * undocumented function
+ *
+ * @param string $str
+ * @param string $options
+ * @return array
+ */
+ public static function parse($str, $options = array()) {
+ $tagTypes = array('todo', 'discuss', 'fix', 'important');
+ $tags = '/@(?P<type>' . join('|', $tagTypes) . ')\s(?P<text>.+)$/mi';
+
+ if (!preg_match_all($tags, $str, $matches, PREG_SET_ORDER ^ PREG_OFFSET_CAPTURE)) {
+ return false;
+ }
+ $r = array();
+
+ 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');
+ }
+ return $r;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/reflection/Inspector.php b/libraries/lithium/util/reflection/Inspector.php
new file mode 100644
index 0000000..ad1f773
--- /dev/null
+++ b/libraries/lithium/util/reflection/Inspector.php
@@ -0,0 +1,385 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util\reflection;
+
+use \Exception;
+use \ReflectionClass;
+use \lithium\core\Libraries;
+use \lithium\util\Collection;
+
+class Inspector extends \lithium\core\StaticObject {
+
+ protected static $_classes = array(
+ 'collection' => '\lithium\util\Collection'
+ );
+
+ /**
+ * Maps reflect method names to result array keys.
+ *
+ * @var array
+ */
+ protected static $_methodMap = array(
+ 'name' => 'getName',
+ 'start' => 'getStartLine',
+ 'end' => 'getEndLine',
+ 'file' => 'getFileName',
+ 'comment' => 'getDocComment',
+ 'namespace' => 'getNamespaceName',
+ 'shortName' => 'getShortName'
+ );
+
+ public static function type($identifier) {
+ if (strpos($identifier, '::')) {
+ return (strpos($identifier, '$') !== false) ? 'property' : 'method';
+ }
+ return class_exists($identifier) ? 'class' : 'namespace';
+ }
+
+ public static function info($identifier, $info = array()) {
+ $info = $info ?: array_keys(static::$_methodMap);
+ $type = static::type($identifier);
+ $result = array();
+ $class = null;
+
+ if ($type == 'method' || $type == 'property') {
+ list($class, $identifier) = explode('::', $identifier);
+ $classInspector = new ReflectionClass($class);
+
+ $modifiers = array('public', 'private', 'protected', 'abstract', 'final', 'static');
+ $result['modifiers'] = array();
+
+ if ($type == 'property') {
+ $identifier = substr($identifier, 1);
+ $accessor = 'getProperty';
+ } else {
+ $identifier = str_replace('()', '', $identifier);
+ $accessor = 'getMethod';
+ }
+
+ try {
+ $inspector = $classInspector->{$accessor}($identifier);
+ } catch (Exception $e) {
+ return null;
+ }
+
+ foreach ($modifiers as $mod) {
+ $m = 'is' . ucfirst($mod);
+ if (method_exists($inspector, $m) && $inspector->{$m}()) {
+ $result['modifiers'][] = $mod;
+ }
+ }
+ } elseif ($type == 'class') {
+ $inspector = new ReflectionClass($identifier);
+ } else {
+ return null;
+ }
+
+ foreach ($info as $key) {
+ if (!isset(static::$_methodMap[$key])) {
+ continue;
+ }
+ if (method_exists($inspector, static::$_methodMap[$key])) {
+ $setAccess = (
+ ($type == 'method' || $type == 'property') &&
+ array_intersect($result['modifiers'], array('private', 'protected')) != array()
+ );
+
+ if ($setAccess) {
+ $inspector->setAccessible(true);
+ }
+ $result[$key] = $inspector->{static::$_methodMap[$key]}();
+
+ if ($setAccess) {
+ $inspector->setAccessible(false);
+ $setAccess = false;
+ }
+ }
+ }
+
+ if (isset($result['start']) && isset($result['end'])) {
+ $result['length'] = $result['end'] - $result['start'];
+ }
+ if (isset($result['comment'])) {
+ $result += Docblock::comment($result['comment']);
+ }
+ return $result;
+ }
+
+ /**
+ * Gets the executable lines of a class, by examining the start and end lines of each method.
+ *
+ * @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.
+ * @return array Returns an array of the executable line numbers of the class.
+ */
+ public static function executable($class, $options = array()) {
+ $defaults = array(
+ 'self' => true, 'filter' => true, 'methods' => array(),
+ 'empty' => array(' ', "\t", '}', ')', ';'), 'pattern' => null,
+ 'blockOpeners' => array('switch (', 'try {', '} else {', 'do {', '} while')
+ );
+ $options += $defaults;
+
+ if (empty($options['pattern']) && $options['filter']) {
+ $pattern = str_replace(' ', '\s*', join('|', array_map(
+ function($str) { return preg_quote($str, '/'); },
+ $options['blockOpeners']
+ )));
+ $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
+ )));
+
+ if ($options['filter'] && $class->getFileName()) {
+ $file = explode("\n", "\n" . file_get_contents($class->getFileName()));
+ $lines = array_intersect_key($file, array_flip($result));
+ $result = array_keys(array_filter($lines, function($line) use ($options) {
+ $line = trim($line);
+ $empty = (strpos($line, '//') === 0 || preg_match($options['pattern'], $line));
+ return $empty ? false : (str_replace($options['empty'], '', $line) != '');
+ }));
+ }
+ return $result;
+ }
+
+ /**
+ * Returns various information on the methods of an object, in different formats.
+ *
+ * @param mixed $class A string class name or an object instance, from which to get methods.
+ * @param string $format The type and format of data to return. Available options are:
+ * -null: Returns a `Collection` object containing a `ReflectionMethod` instance
+ * for each method.
+ * -'extents': Returns a two-dimensional array with method names as keys, and
+ * an array with starting and ending line numbers as values.
+ * -'ranges': Returns a two-dimensional array where each key is a method name,
+ * and each value is an array of line numbers which are contained in the method.
+ * @param array $options
+ */
+ public static function methods($class, $format = null, $options = array()) {
+ $defaults = array('methods' => array(), 'group' => true, 'self' => true);
+ $options += $defaults;
+
+ if (!(is_object($class) && $class instanceof ReflectionClass)) {
+ $class = new ReflectionClass($class);
+ }
+ $methods = static::_methods($class, $options);
+ $result = array();
+
+ switch ($format) {
+ case null:
+ return $methods;
+ case 'extents':
+ if ($methods->getName() == array()) {
+ return array();
+ }
+
+ $extents = function($start, $end) { return array($start, $end); };
+ $result = array_combine($methods->getName(), array_map(
+ $extents, $methods->getStartLine(), $methods->getEndLine()
+ ));
+ break;
+ case 'ranges':
+ $ranges = function($lines) {
+ list($start, $end) = $lines;
+ return ($end <= $start + 1) ? array() : range($start + 1, $end - 1);
+ };
+ $result = array_map($ranges, static::methods(
+ $class, 'extents', array('group' => true) + $options
+ ));
+ break;
+ }
+
+ if ($options['group']) {
+ return $result;
+ }
+ $tmp = $result;
+ $result = array();
+
+ array_map(function($ln) use (&$result) { $result = array_merge($result, $ln); }, $tmp);
+ return $result;
+ }
+
+ /**
+ * Returns an array of lines from a file, class, or arbitrary string, where $data is the data
+ * to read the lines from and $lines is an array of line numbers specifying which lines should
+ * be read.
+ *
+ * @param string $data If `$data` contains newlines, it will be read from directly, and have
+ * its own lines returned. If `$data` is a physical file path, that file will be
+ * read and have its lines returned. If `$data` is a class name, it will be
+ * converted into a physical file path and read.
+ * @param array $lines The array of lines to read. If a given line is not present in the data,
+ * it will be silently ignored.
+ * @return array Returns an array where the keys are matching `$lines`, and the values are the
+ * corresponding line numbers in `$data`.
+ * @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 (!file_exists($data)) {
+ $data = Libraries::path($data);
+ if (!file_exists($data)) {
+ return null;
+ }
+ }
+ $data = "\n" . file_get_contents($data);
+ }
+ $c = explode("\n", $data);
+
+ if (!count($c) || !count($lines)) {
+ return null;
+ }
+ return array_intersect_key($c, array_combine($lines, array_fill(0, count($lines), null)));
+ }
+
+ /**
+ * Gets the full inheritance list for the given class.
+ *
+ * @param string $class
+ * @param array $options
+ */
+ public static function parents($class, $options = array()) {
+ $defaults = array('autoLoad' => false);
+ $options += $defaults;
+ $class = is_object($class) ? get_class($class) : $class;
+
+ if (!class_exists($class, $options['autoLoad'])) {
+ return false;
+ }
+ return class_parents($class);
+ }
+
+ /**
+ * Gets an array of classes and their corresponding definition files, or examines a file and
+ * returns the classes it defines.
+ *
+ * @param array $options
+ * @return array
+ */
+ public static function classes($options = array()) {
+ $defaults = array('group' => 'classes', 'file' => null);
+ $options += $defaults;
+
+ $list = get_declared_classes();
+ $classes = array();
+
+ if (!empty($options['file'])) {
+ $loaded = new Collection(array('items' => array_map(
+ function($class) { return new ReflectionClass($class); }, $list
+ )));
+
+ if (!in_array($options['file'], $loaded->getFileName())) {
+ include $options['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();
+ }
+ }
+
+ foreach ($list as $class) {
+ $inspector = new ReflectionClass($class);
+
+ if ($options['group'] == 'classes') {
+ $inspector->getFileName() ? $classes[$class] = $inspector->getFileName() : null;
+ } elseif ($options['group'] == 'files') {
+ $classes[$inspector->getFileName()][] = $inspector;
+ }
+ }
+ return $classes;
+ }
+
+ /**
+ * Gets the static and dynamic dependencies for a class or group of classes.
+ *
+ * @package default
+ */
+ public static function dependencies($classes, $options = array()) {
+ $defaults = array('type' => null);
+ $options += $defaults;
+ $static = $dynamic = array();
+ $trim = function($c) { return trim(trim($c, '\\')); };
+ $join = function ($i) { return join('', $i); };
+
+ foreach ((array)$classes as $class) {
+ $data = file_get_contents(Libraries::path($class));
+ $classes = array_map($join, Parser::find($data, 'use *;', array(
+ 'return' => 'content',
+ 'lineBreaks' => true,
+ 'startOfLine' => true,
+ 'capture' => array('T_STRING', 'T_NS_SEPARATOR')
+ )));
+
+ if ($classes) {
+ $static = array_unique(array_merge($static, array_map($trim, $classes)));
+ }
+ $classes = static::info($class . '::$_classes', array('value'));
+
+ if (isset($classes['value'])) {
+ $dynamic = array_merge($dynamic, array_map($trim, $classes['value']));
+ }
+ }
+
+ if (empty($options['type'])) {
+ return array_unique(array_merge($static, $dynamic));
+ }
+ $type = $options['type'];
+ return isset(${$type}) ? ${$type} : null;
+ }
+
+ /**
+ * Helper method to get an array of `ReflectionMethod` objects, wrapped in a `Collection`
+ * object, and filtered based on a set of options.
+ *
+ * @param ReflectionClass $class A reflection class instance from which to fetch.
+ * @param array $options The options used to filter the resulting method list.
+ */
+ protected static function _methods($class, $options) {
+ $defaults = array('methods' => array(), 'self' => true, 'public' => true);
+ $options += $defaults;
+ $methods = $class->getMethods();
+
+ if (!empty($options['methods'])) {
+ $methods = array_filter($methods, function($method) use ($options) {
+ return in_array($method->getName(), (array)$options['methods']);
+ });
+ }
+
+ if ($options['self']) {
+ $methods = array_filter($methods, function($method) use ($class) {
+ return ($method->getDeclaringClass()->getName() == $class->getName());
+ });
+ }
+
+ if ($options['public']) {
+ $methods = array_filter($methods, function($method) { return $method->isPublic(); });
+ }
+ return new static::$_classes['collection'](array('items' => $methods));
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/reflection/Parser.php b/libraries/lithium/util/reflection/Parser.php
new file mode 100644
index 0000000..fa0c340
--- /dev/null
+++ b/libraries/lithium/util/reflection/Parser.php
@@ -0,0 +1,273 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+
+namespace lithium\util\reflection;
+
+use \ReflectionClass;
+use \lithium\core\Libraries;
+use \lithium\util\Collection;
+use \lithium\util\Validator;
+use \lithium\util\Set;
+
+class Parser extends \lithium\core\StaticObject {
+
+ /**
+ * Convenience method to get the token name of a PHP code string. If multiple tokens are
+ * present in the string, only the first is returned.
+ *
+ * @param string $string String of PHP code to get the token name of, i.e. `'=>'` or `'static'`.
+ * @param string $options
+ * @return mixed
+ */
+ public static function token($string, $options = array()) {
+ $defaults = array('id' => false);
+ $options += $defaults;
+
+ if (empty($string) && $string !== '0') {
+ return false;
+ }
+ list($token) = static::tokenize($string);
+ return $token[($options['id']) ? 'id' : 'name'];
+ }
+
+ public static function tokenize($code, $options = array()) {
+ $defaults = array('wrap' => true, 'ignore' => array());
+ $options += $defaults;
+ $tokens = array();
+ $line = 1;
+
+ if ($options['wrap']) {
+ $code = "<?php {$code}?>";
+ }
+
+ foreach (token_get_all($code) as $token) {
+ $token = is_array($token) ? $token : array(null, $token, $line);
+ list($id, $content, $line) = $token;
+ $name = $id ? token_name($id) : $content;
+
+ if (in_array($name, $options['ignore']) || in_array($id, $options['ignore'])) {
+ continue;
+ }
+ $tokens[] = compact('id', 'name', 'content', 'line');
+ }
+
+ if ($options['wrap']) {
+ $tokens = array_slice($tokens, 1, count($tokens) - 2);
+ }
+ return $tokens;
+ }
+
+ /**
+ * Finds a pattern in a block of code.
+ *
+ * @param string $code
+ * @param string $pattern
+ * @param array $options The list of options to be used when parsing / matching `$code`:
+ * - 'ignore': An array of token names to ignore while parsing, defaults to
+ * `array('T_WHITESPACE')`
+ * - 'lineBreaks': If true, all tokens in a single pattern match must appear on the
+ * same line of code, defaults to false
+ * - 'startOfLine': If true, the pattern must match starting with the beginning of
+ * the line of code to be matched, defaults to false
+ * @return array
+ */
+ public static function find($code, $pattern, $options = array()) {
+ $defaults = array(
+ 'all' => true, 'capture' => array(), 'ignore' => array('T_WHITESPACE'),
+ 'return' => true, 'lineBreaks' => false, 'startOfLine' => false
+ );
+ $options += $defaults;
+ $results = array();
+ $matches = array();
+ $patternMatch = array();
+ $ret = $options['return'];
+
+ $tokens = new Collection(array('items' => static::tokenize($code, $options)));
+ $pattern = new Collection(array('items' => static::tokenize($pattern, $options)));
+
+ $breaks = function($token) use (&$tokens, &$matches, &$patternMatch, $options) {
+ if (!$options['lineBreaks']) {
+ return true;
+ }
+ if (empty($patternMatch) && !$options['startOfLine']) {
+ return true;
+ }
+
+ if (empty($patternMatch)) {
+ $prev = $tokens->prev();
+ $tokens->next();
+ } else {
+ $prev = reset($patternMatch);
+ }
+
+ if (empty($patternMatch) && $options['startOfLine']) {
+ return ($token['line'] > $prev['line']);
+ }
+ return ($token['line'] == $prev['line']);
+ };
+
+ $capture = function($token) use (&$matches, &$patternMatch, $tokens, $breaks, $options) {
+ if (is_null($token)) {
+ $matches = $patternMatch = array();
+ return false;
+ }
+
+ if (empty($patternMatch)) {
+ $prev = $tokens->prev();
+ $tokens->next();
+ if ($options['startOfLine'] && $token['line'] == $prev['line']) {
+ $patternMatch = $matches = array();
+ return false;
+ }
+ }
+ $patternMatch[] = $token;
+
+ if (empty($options['capture']) || !in_array($token['name'], $options['capture'])) {
+ return true;
+ }
+ if (!$breaks($token)) {
+ $matches = array();
+ return true;
+ }
+ $matches[] = $token;
+ return true;
+ };
+
+ $executors = array(
+ '*' => function(&$tokens, &$pattern) use ($options, $capture) {
+ $closing = $pattern->next();
+ $tokens->prev();
+
+ while (($t = $tokens->next()) && !Parser::matchToken($closing, $t)) {
+ $capture($t);
+ }
+ $pattern->next();
+ }
+ );
+
+ $tokens->rewind();
+ $pattern->rewind();
+
+ while ($tokens->valid()) {
+ if (!$pattern->valid()) {
+ $pattern->rewind();
+
+ if (!empty($matches)) {
+ $results[] = array_map(
+ function($i) use ($ret) { return isset($i[$ret]) ? $i[$ret] : $i; },
+ $matches
+ );
+ }
+ $capture(null);
+ }
+
+ $p = $pattern->current();
+ $t = $tokens->current();
+
+ switch (true) {
+ case (static::matchToken($p, $t)):
+ $capture($t) ? $pattern->next() : $pattern->rewind();
+ break;
+ case (isset($executors[$p['name']])):
+ $exec = $executors[$p['name']];
+ $exec($tokens, $pattern);
+ break;
+ default:
+ $capture(null);
+ $pattern->rewind();
+ break;
+ }
+ $tokens->next();
+ }
+ return $results;
+ }
+
+ public static function match($code, $parameters, $options = array()) {
+ $defaults = array('ignore' => array('T_WHITESPACE'), 'return' => true);
+ $options += $defaults;
+ $parameters = static::_prepareMatchParams($parameters);
+
+ $tokens = is_array($code) ? $code : static::tokenize($code, $options);
+ $results = array();
+
+ foreach ($tokens as $i => $token) {
+ if (!array_key_exists($token['name'], $parameters)) {
+ if (!in_array('*', $parameters)) {
+ continue;
+ }
+ }
+ $param = $parameters[$token['name']];
+
+ if (isset($param['before']) && $i > 0) {
+ if (!in_array($tokens[$i - 1]['name'], (array)$param['before'])) {
+ continue;
+ }
+ }
+
+ if (isset($param['after']) && $i + 1 < count($tokens)) {
+ if (!in_array($tokens[$i + 1]['name'], (array)$param['after'])) {
+ continue;
+ }
+ }
+ $results[] = isset($token[$options['return']]) ? $token[$options['return']] : $token;
+ }
+ return $results;
+ }
+
+ public static function matchToken($pattern, $token) {
+ if ($pattern['name'] != $token['name']) {
+ return false;
+ }
+
+ if (!isset($pattern['content'])) {
+ return true;
+ }
+
+ $match = $pattern['content'];
+ $content = $token['content'];
+
+ if ($pattern['name'] == 'T_VARIABLE') {
+ $match = substr($match, 1);
+ $content = substr($content, 1);
+ }
+
+ switch (true) {
+ case ($match == '_' || $match == $content):
+ return true;
+ }
+ return false;
+ }
+
+ protected static function _prepareMatchParams($parameters) {
+ foreach (Set::normalize($parameters) as $token => $scope) {
+ if (strpos($token, 'T_') !== 0) {
+ unset($parameters[$token]);
+
+ foreach (array('before', 'after') as $key) {
+ if (!isset($scope[$key])) {
+ continue;
+ }
+ $items = array();
+
+ foreach ((array)$scope[$key] as $item) {
+ $items[] = (strpos($item, 'T_') !== 0) ? static::token($item) : $item;
+ }
+ $scope[$key] = $items;
+ }
+ $parameters[static::token($token)] = $scope;
+ }
+ }
+ return $parameters;
+ }
+}
+
+?>
\ No newline at end of file
diff --git a/libraries/lithium/util/socket/Curl.php b/libraries/lithium/util/socket/Curl.php
new file mode 100644
index 0000000..4984691
--- /dev/null
+++ b/libraries/lithium/util/socket/Curl.php
@@ -0,0 +1,91 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\util\socket;
+
+class Curl extends \lithium\util\Socket {
+
+ public $options = array();
+
+ public function open() {
+ $config = $this->_config;
+
+ if (empty($config['protocol']) || empty($config['host'])) {
+ return false;
+ }
+
+ $url = "{$config['protocol']}://{$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_RETURNTRANSFER, true);
+
+ if (is_resource($this->_resource)) {
+ $this->_isConnected = true;
+
+ $this->timeout($config['timeout']);
+
+ if (!empty($config['encoding'])) {
+ $this->encoding($config['encoding']);
+ }
+ }
+
+ return $this->_resource;
+ }
+
+ public function close() {
+ if (!is_resource($this->_resource)) {
+ return true;
+ }
+ curl_close($this->_resource);
+ if (is_resource($this->_resource)) {
+ $this->close();
+ }
+ return true;
+ }
+
+ public function eof() {
+
+ }
+
+ public function read() {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ curl_setopt_array($this->_resource, $this->options);
+ return curl_exec($this->_resource);
+ }
+
+ public function write($data) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ curl_setopt_array($this->_resource, $this->options);
+ return curl_exec($this->_resource);
+ }
+
+ public function timeout($time) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ curl_setopt($this->_resource, CURLOPT_CONNECTTIMEOUT, $time);
+ }
+
+ public function encoding($charset) {
+ }
+
+ public function set($flags, $value = null) {
+ if ($value !== null) {
+ $flags = array($flags => $value);
+ }
+ $this->options += $flags;
+ }
+}
\ No newline at end of file
diff --git a/libraries/lithium/util/socket/Stream.php b/libraries/lithium/util/socket/Stream.php
new file mode 100644
index 0000000..ec1812e
--- /dev/null
+++ b/libraries/lithium/util/socket/Stream.php
@@ -0,0 +1,104 @@
+<?php
+/**
+ * Lithium: the most rad php framework
+ * Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ *
+ * Licensed under The BSD License
+ * Redistributions of files must retain the above copyright notice.
+ *
+ * @copyright Copyright 2009, Union of Rad, Inc. (http://union-of-rad.org)
+ * @license http://opensource.org/licenses/bsd-license.php The BSD License
+ */
+namespace lithium\util\socket;
+
+use \Exception;
+
+class Stream extends \lithium\util\Socket {
+
+ public function open() {
+ $config = $this->_config;
+
+ if (empty($config['protocol']) || empty($config['host'])) {
+ return false;
+ }
+
+ $host = "{$config['protocol']}://{$config['host']}";
+
+ if ($config['persistent']) {
+ $this->_resource = pfsockopen($host, $config['port'], $errorCode, $errorMessage);
+ } else {
+ $this->_resource = fsockopen($host, $config['port'], $errorCode, $errorMessage);
+ }
+
+ if (!empty($errorCode) || !empty($errorMessage)) {
+ throw new Exception($errorMessage, $errorCode);
+ }
+
+ $this->timeout($config['timeout']);
+
+ if (!empty($config['encoding'])) {
+ $this->encoding($config['encoding']);
+ }
+
+ return $this->_resource;
+ }
+
+ public function close() {
+ if (!is_resource($this->_resource)) {
+ return true;
+ }
+ fclose($this->_resource);
+ if (is_resource($this->_resource)) {
+ $this->close();
+ }
+ return true;
+ }
+
+ public function eof() {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ return feof($this->_resource);
+ }
+
+ public function read($length = null, $offset = null) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+
+ if (is_null($length)) {
+ $buffer = stream_get_contents($this->_resource);
+ } else {
+ $buffer = stream_get_contents($this->_resource, $length, $offset);
+ }
+
+ $info = stream_get_meta_data($this->_resource);
+ if (empty($info['timed_out'])) {
+ return $buffer;
+ }
+ return false;
+ }
+
+ public function write($data) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ return fwrite($this->_resource, $data, strlen($data));
+ }
+
+ public function timeout($time) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ stream_set_timeout($this->_resource, $time);
+ }
+
+ public function encoding($charset) {
+ if (function_exists('stream_encoding')) {
+ if (!is_resource($this->_resource)) {
+ return false;
+ }
+ stream_encoding($this->_resource, $charset);
+ }
+ }
+}
\ No newline at end of file