Commit: e7f3c3137a4c543223b59f61e41b2f5dedbb7bb0

Author: gwoo | Date: 2009-10-12 19:46:17 -0700
going lithium
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 . "&amp;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}>&nbsp;</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&param1=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 = '&lt;script&gt;alert(&quot;XSS!&quot;);&lt;/script&gt;'; + $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 &gt;&gt;', + '/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 &gt;', '/a'); + $this->assertTags($result, $expected); + + $result = $this->html->link('Next >', '#', array('escape' => true)); + $expected = array( + 'a' => array('href' => '#'), + 'Next &gt;', + '/a' + ); + $this->assertTags($result, $expected); + + $result = $this->html->link('Next >', '#', array('escape' => 'utf-8')); + $expected = array( + 'a' => array('href' => '#'), + 'Next &gt;', + '/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 &#8230; or not escape?', + 'escape' => false + )); + $expected = array( + 'a' => array('href' => '#', 'title' => 'to escape &#8230; or not escape?'), + 'Next >', + '/a' + ); + $this->assertTags($result, $expected); + + $result = $this->html->link('Next >', '#', array( + 'title' => 'to escape &#8230; or not escape?', 'escape' => true + )); + $expected = array( + 'a' => array('href' => '#', 'title' => 'to escape &amp;#8230; or not escape?'), + 'Next &gt;', + '/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&amp;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'), '&lt;text&gt;', '/div' + )); + + $result = $this->html->tag('div', '<text>', 'class-name', true); + $this->assertTags($result, array( + 'div' => array('class' => 'class-name'), '&lt;text&gt;', '/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'), '&lt;text&gt;', '/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'), '&lt;text&gt;', '/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 = '&lt;script&gt;alert(&quot;XSS!&quot;);&lt;/script&gt;'; + + $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