kolibri-discuss team mailing list archive
-
kolibri-discuss team
-
Mailing list archive
-
Message #00046
[Merge] lp:~asteinlein/kolibri/nested-routes-mapper into lp:kolibri
Anders Steinlein has proposed merging lp:~asteinlein/kolibri/nested-routes-mapper into lp:kolibri.
Requested reviews:
Kolibri Dev (kolibri-dev)
This branch has developed into more of a "new stuff" branch than only nested routes, so a look through all changes is required. However, the most important changes (apart from a few bugfixes) are the nested routes and support for Selenium integration testing, both of which are currently being used in development of our MailMojo app.
--
https://code.launchpad.net/~asteinlein/kolibri/nested-routes-mapper/+merge/6966
Your team Kolibri Discuss is subscribed to branch lp:kolibri.
=== renamed file 'examples/wishlist/actions/items/Add.php' => 'examples/wishlist/actions/items/ItemsAdd.php'
--- examples/wishlist/actions/items/Add.php 2009-04-14 16:50:50 +0000
+++ examples/wishlist/actions/items/ItemsAdd.php 2009-05-10 13:27:03 +0000
@@ -3,7 +3,7 @@
* Action for adding new item. We implement ModelAware to have the model object populated by
* request data, and ValidationAware to have it automatically validated.
*/
-class Add implements MessageAware, ModelAware, ValidationAware {
+class ItemsAdd implements MessageAware, ModelAware, ValidationAware {
/**
* Defines the model class to instantiate, which will be populated with request data and
* put back into this property.
=== renamed file 'examples/wishlist/actions/items/Del.php' => 'examples/wishlist/actions/items/ItemsDel.php'
--- examples/wishlist/actions/items/Del.php 2009-04-15 21:10:58 +0000
+++ examples/wishlist/actions/items/ItemsDel.php 2009-05-10 13:27:03 +0000
@@ -3,13 +3,13 @@
* Action for deleting items. As the item to delete is specified by the last URI element (which
* does not have a matching action file), it is implicitly put in the "id" request parameter.
*/
-class Del implements MessageAware {
+class ItemsDel implements MessageAware {
/**
* TODO: We should really POST the form (and thus doPost()).
*/
public function doGet ($request) {
// We could also do $request->get('id'), whatever you prefer
- $itemName = $request['id'];
+ $itemName = $request['itemsid'];
$item = Models::init('Item');
// Tries to load the item (notice that this calls load() in the ItemDao class)
=== renamed file 'examples/wishlist/actions/items/Have.php' => 'examples/wishlist/actions/items/ItemsHave.php'
--- examples/wishlist/actions/items/Have.php 2009-04-15 19:55:02 +0000
+++ examples/wishlist/actions/items/ItemsHave.php 2009-05-10 13:27:03 +0000
@@ -2,13 +2,13 @@
/**
* Action for marking items as received.
*/
-class Have implements MessageAware {
+class ItemsHave implements MessageAware {
/**
* TODO: We should really POST the form (and thus doPost()).
*/
public function doGet ($request) {
$item = Models::init('Item');
- if ($item->objects->load($request['id'])) {
+ if ($item->objects->load($request['itemsid'])) {
// Set received date (could be done in SQL, but just to show date library in use)
$df = DateFormat::getInstance(DateFormat::ISO_8601_DATE);
$item->received = $df->format(new Date());
@@ -17,7 +17,7 @@
$this->msg->setMessage('Item successfully marked as received.');
}
else {
- $this->msg->setMessage("Item with name {$request['id']} not found.", false);
+ $this->msg->setMessage("Item with name {$request['itemsid']} not found.", false);
}
return new RedirectResponse('/');
=== modified file 'examples/wishlist/views/index.xsl'
--- examples/wishlist/views/index.xsl 2008-12-17 15:50:47 +0000
+++ examples/wishlist/views/index.xsl 2009-05-10 13:27:03 +0000
@@ -49,9 +49,9 @@
<span class="price">Price: <xsl:value-of select="price" /></span>
</xsl:if>
<p class="actions">
- <a href="{$webRoot}/items/have/{name}">Got it!</a>
+ <a href="{$webRoot}/items/{name}/have">Got it!</a>
/
- <a href="{$webRoot}/items/del/{name}">Nah, don't want anymore</a>
+ <a href="{$webRoot}/items/{name}/del">Nah, don't want anymore</a>
</p>
</div>
</xsl:template>
@@ -62,7 +62,7 @@
<h3><xsl:value-of select="name" /></h3>
<p><xsl:value-of select="description" /></p>
<p class="actions">
- <a href="{$webRoot}/items/del/{name}">I lost it :-(</a>
+ <a href="{$webRoot}/items/{name}/del">I lost it :-(</a>
</p>
</div>
</xsl:template>
=== modified file 'src/core/ActionMapping.php'
--- src/core/ActionMapping.php 2008-10-20 16:41:10 +0000
+++ src/core/ActionMapping.php 2009-05-10 13:27:03 +0000
@@ -2,12 +2,8 @@
/**
* This class encapsulates information about the mapping to an action.
*
- * The mapping to an action consists of the name of an <code>ActionHandler</code> class and an
- * action within that handler. The path to the action is also availible to ease comparing mappings
- * (i.e. for interceptors). Finally, this class also flags whether the action mapping described by
- * the instance is considered valid. A valid action mapping may be invoked.
- *
- * @version $Id: ActionMapping.php 1478 2008-04-02 14:04:52Z anders $
+ * The mapping to an action consists of the name of the action class and the action method
+ * within that class.
*/
class ActionMapping {
/**
@@ -23,19 +19,21 @@
private $actionMethod;
/**
- * Full path to the action class. (Do we care about this?)
+ * Full path to the action class.
+ * XXX: This is currently unused except when requiring the file. Do we care about this?
* @var string
*/
private $actionPath;
/**
- * Creates an instance of this class. An exception is thrown if the action method is not callable.
+ * Creates an instance of this class. An exception is thrown if the action method is not
+ * callable.
*
* @param string $actionPath Full path to the action class.
- * @param string $actionMethod Name of the action method in the action class. One of doGet or doPost.
+ * @param string $actionMethod Name of the action method in the action class.
*/
public function __construct ($actionPath, $actionMethod) {
- require_once($actionPath);
+ require($actionPath);
$this->actionPath = $actionPath;
$this->actionClass = basename($actionPath, '.php');
$this->actionMethod = $actionMethod;
=== modified file 'src/core/DefaultActionMapper.php'
--- src/core/DefaultActionMapper.php 2009-04-16 22:03:20 +0000
+++ src/core/DefaultActionMapper.php 2009-05-13 09:22:55 +0000
@@ -4,13 +4,20 @@
/**
* This class is responsible for mapping a request URI to a target action.
*
- * This implementation maps the
- * request URI to an action by first traversing the URI parts (the strings between the / characters)
- * and looking for an action handler ........
- *
- * TODO: Complete comment
- *
- * @version $Id: DefaultActionMapper.php 1530 2008-07-21 15:10:08Z anders $
+ * This implementation maps the URI to an action by traversing the URI parts (the strings
+ * between the / characters) and searching the application's /actions directory for matching
+ * directories and files. Parts not explicitly matched are assumed to be IDs unless we have
+ * already set an ID for the current URI "section". URI parts after the part that matched
+ * a file are also mapped to parameters. The request method determines the actual method
+ * to call on the action class, either <code>doGet()</code> or <code>doPost()</code>. This is
+ * all easier to explain with a couple of examples:
+ *
+ * Given the URI: /lists
+ * Matches the action class: Lists.php <em>or</em> /lists/ListsIndex.php
+ *
+ * Given the URI: /lists/1/contacts/4
+ * Matches the action class: /lists/contacts/ListsContactsView.php
+ * And sets request parameters: listsid=1 <em>and</em> contactsid=4
*/
class DefaultActionMapper {
/**
@@ -26,6 +33,12 @@
protected $mapping;
/**
+ * Parts of the URI which we map into request parameters are temporarily stored here.
+ * @var array
+ */
+ protected $params;
+
+ /**
* Create an instance of this class and initialize an <code>ActionMapping</code>.
*/
public function __construct ($request) {
@@ -36,30 +49,25 @@
* Maps the request to its target action and returns the <code>ActionMapping</code>.
* If the request could not be mapped to an action, <code>NULL</code> is returned.
*
- * @return object <code>ActionMapping</code> representing the action mapped to the request,
- * or <code>NULL</code> if no action could be mapped.
+ * @return object <code>ActionMapping</code> representing the action mapped to the
+ * request, or <code>null</code> if no action could be mapped.
*/
public function map () {
- /*
- * First strip scheme and hostname from webRoot, to ensure webRoot is an URI path like
- * the request URI. We then "normalize" the URI to be within the webRoot.
- */
- $absoluteUri = parse_url(Config::get('webRoot'), PHP_URL_PATH);
- $uri = trim(str_replace($absoluteUri, '', $this->request->getUri()), '/');
+ // Remove any prepending / and explode URI into its parts
+ $uri = ltrim($this->request->getUri());
$uriParts = (empty($uri) ? array() : explode('/', $uri));
// Map the URI to its target action
$actionPath = $this->mapAction($uriParts);
/*
- * If actionPath is not null, then the URI matched a PHP file. We can then map the action method
- * and parameters and create the action mapping.
+ * If actionPath is not null, then the URI matched a PHP file. We can then map the
+ * action method and parameters and create the action mapping.
*/
if ($actionPath !== null) {
$actionMethod = $this->mapMethod($this->request->getMethod());
$this->mapping = new ActionMapping($actionPath, $actionMethod);
$this->mapParams($uriParts);
-
return $this->mapping;
}
@@ -71,8 +79,8 @@
* Maps the request method to an action method. We currently only support GET and POST.
*
* @param string $method Request method.
+ * @return string Action method to use.
* @throws Exception If the request method is unsupported.
- * @return string Action method to use.
*/
protected function mapMethod ($method) {
if ($method == 'GET') {
@@ -87,49 +95,72 @@
}
/**
- * Maps the action handler to handle the request specified by the supplied URI.
+ * Maps the action class to handle the request specified by the supplied URI.
*
* @param array $uri The request URI in an array.
- * @return string The full file system path to the action which is to handle the
- * request.
+ * @return string The absolute file system path to the action which is to handle the
+ * request.
*/
protected function mapAction (&$uri) {
- // Get the initial path to the handlers directory
- $actionPath = ACTIONS_PATH . '/';
- $index = 0;
+ $actionClassPath = ACTIONS_PATH . '/';
+ $actionClass = '';
+ $previousPart = '';
// Loop through the URI parts and look for a suitable action
foreach ($uri as $part) {
- if ($part != '' && is_dir($actionPath . $part)) {
- // Current part is a directory, append to action path and shift directory off the URI
- $actionPath .= $part . '/';
+ // "CamelCase"-fix for action class name
+ $cameledPart = implode(array_map('ucfirst', explode('_', $part)));
+
+ // Check if the classpath + part is a directory
+ if ($part != '' && is_dir($actionClassPath . $part)) {
+ $actionClassPath .= $part . '/';
+ $actionClass .= $cameledPart;
+ $previousPart = $part; // Remember this part for next iteration
+
array_shift($uri);
}
else {
- // "CamelCase"-fix for action name.
- $actionName = implode(array_map('ucfirst', explode('_', $part)));
- $actionFile = $actionName . '.php';
+ // For convenience, fix basename of possible action file at this point
+ $actionFile = $actionClass . $cameledPart . '.php';
- if (is_file($actionPath . $actionFile)) {
- // Element is specific action. Append element to action path.
- $actionPath .= $actionFile;
+ // Check if the classpath + part is a file
+ if (is_file($actionClassPath . $actionFile)) {
+ $actionClassPath .= $actionFile;
+ $actionClass .= $cameledPart;
- // Shift the action name off URI
- array_shift($uri);
- return $actionPath;
- }
-
- break; // Current part was not found as action, break out of loop to try default action
+ array_shift($uri);
+
+ // This part did indeed lead to a specific file, so we return with
+ return $actionClassPath;
+ }
+ /*
+ * Else if a previous part has been set, that means that the previous part
+ * matched a directory, and we can set the current as an id related to that
+ * "section".
+ */
+ else if ($previousPart !== null) {
+ $this->params[$previousPart . 'id'] = $part;
+
+ // Reset previous part, to disallow several ids for same "section"
+ $previousPart = null;
+ array_shift($uri);
+ }
+ /*
+ * Otherwise, the current part could not be matched or set as an id, so we
+ * break out of loop to try a default action.
+ */
+ else break;
}
}
/*
- * We are here if the latest URI part that was matched is an existing directory. Check to see
- * if a default action within that directory exists.
+ * We are here if the URI didn't match a specific action class file. If no "previous
+ * part" is present, it means we have an id of which to view a single "item", otherwise
+ * no specific "item" is requested and we look for an index action.
*/
- $actionPath .= 'Index.php';
- if (is_file($actionPath)) {
- return $actionPath;
+ $actionClassPath .= $actionClass . ($previousPart === null ? 'View.php' : 'Index.php');
+ if (is_file($actionClassPath)) {
+ return $actionClassPath;
}
return null;
@@ -138,27 +169,25 @@
/**
* Maps any URI-specified parameters to the request.
*
- * @param array $uri The remaining elements of the request URI (action has been sliced away),
- * specifying the parameters to put in the request.
+ * @param array $uri The remaining elements of the request URI (action has been sliced
+ * away), specifying the parameters to put in the request.
*/
protected function mapParams ($uri) {
- $params = array();
-
// Step through parameters in the URI, two params at a time (as we map key/value pairs)
for ($i = 0; $i < count($uri); $i += 2) {
if (isset($uri[$i + 1])) {
// There is a next param, take current as key and next as value
- $params[urldecode($uri[$i])] = urldecode($uri[$i + 1]);
+ $this->params[urldecode($uri[$i])] = urldecode($uri[$i + 1]);
}
else {
// No next param availible, use the current param as a value with "id" as key
- $params['id'] = urldecode($uri[$i]);
+ $this->params['id'] = urldecode($uri[$i]);
}
}
- if (!empty($params)) {
+ if (!empty($this->params)) {
// TODO: Do we want URI-params to overwrite GET-params as now, or not?
- $this->request->putAll($params);
+ $this->request->putAll($this->params);
}
}
}
=== modified file 'src/core/Dispatcher.php'
--- src/core/Dispatcher.php 2009-04-15 22:31:30 +0000
+++ src/core/Dispatcher.php 2009-05-13 14:49:34 +0000
@@ -73,7 +73,7 @@
}
$class = $actionMapping->getActionClass();
- $this->action = new $class();
+ $this->action = new $class($this->request);
$this->stack = InterceptorFactory::createInterceptors($stack);
}
=== modified file 'src/core/Request.php'
--- src/core/Request.php 2009-04-20 16:03:03 +0000
+++ src/core/Request.php 2009-05-13 09:22:55 +0000
@@ -44,12 +44,20 @@
// If $uri is empty initialize this request with the URI from the client request
if (empty($uri)) {
- $uri = $_SERVER['REQUEST_URI'];
-
+ /*
+ * First strip scheme and hostname from webRoot, to ensure webRoot is an URI path
+ * like the request URI. We then "normalize" the URI to be within the webRoot.
+ */
+ $absoluteUri = parse_url(Config::get('webRoot'), PHP_URL_PATH);
+ $uri = str_replace($absoluteUri, '', $_SERVER['REQUEST_URI']);
+
// Strip any ?-type GET parameters from the URI (they are in the parameters)
if (($paramPos = strpos($uri, '?')) !== false) {
$uri = substr($uri, 0, $paramPos);
}
+
+ // Strip ending / to ensure URIs with or without are handles alike
+ $uri = rtrim($uri, '/');
}
// We use rawurldecode() instead of urldecode() to preserve + in URI
=== modified file 'src/database/ObjectBuilder.php'
--- src/database/ObjectBuilder.php 2009-04-03 16:00:20 +0000
+++ src/database/ObjectBuilder.php 2009-05-18 15:15:52 +0000
@@ -298,8 +298,12 @@
}
if ($isPopulated) {
- // Set the "original"-property to PK value so we know this object can be UPDATEd
+ /*
+ * Set the "original"-property to PK value so we know this object can be UPDATEd,
+ * but in a non-dirty state unless modified.
+ */
$object->original = $row[$this->primaryKeys[$objClass]];
+ $object->isDirty = false;
}
return $isPopulated;
=== modified file 'src/interceptors/AuthInterceptor.php'
--- src/interceptors/AuthInterceptor.php 2009-04-15 22:31:30 +0000
+++ src/interceptors/AuthInterceptor.php 2009-06-02 12:11:36 +0000
@@ -43,13 +43,12 @@
$user = $request->session[$this->userKey];
if (!$this->isUserAuthenticated($user)) {
- $request->session['target'] = $request->getUri();
if ($action instanceof MessageAware) {
$action->msg->setMessage('You must log in to access
the page you requested.', false);
}
- return $this->denyAccess();
+ return $this->denyAccess($request->getUri());
}
if ($action instanceof AuthAware) {
@@ -89,9 +88,17 @@
/**
* Denies access by redirecting to the configured login URI.
+ *
+ * @param string $target Optional target of the request; the resource where access was
+ * denied.
*/
- private function denyAccess () {
- return new RedirectResponse($this->loginUri);
+ private function denyAccess ($target = null) {
+ $redirectTo = $this->loginUri;
+ if (!empty($target)) {
+ $redirectTo .= "?target=$target";
+ }
+
+ return new RedirectResponse($redirectTo);
}
}
?>
=== modified file 'src/interceptors/ModelInterceptor.php'
--- src/interceptors/ModelInterceptor.php 2009-04-14 16:50:50 +0000
+++ src/interceptors/ModelInterceptor.php 2009-05-18 15:15:52 +0000
@@ -51,6 +51,15 @@
if (method_exists($action, 'getModel')) {
// The action supplies an already instantiated model
$model = $action->getModel();
+
+ /*
+ * During population of the model, we perform a property_exists() to
+ * ensure that a parameter is indeed a property on the model. For this
+ * to work the model must be a plain model; so we extract any proxy.
+ */
+ if ($model instanceof ModelProxy) {
+ $model = $model->extract();
+ }
}
else {
// The action supplies model class name(s), so we must instantiate
@@ -68,8 +77,12 @@
$this->populate($model, $exploded, $value);
}
else {
+ /*
+ * "original" is usually not actually defined, but we still want
+ * it set when availible.
+ */
if (property_exists($model, $param) || $param == 'original') {
- $model->$param = $this->convertType($value);
+ $this->setProperty($model, $param, $value);
}
}
}
@@ -84,11 +97,12 @@
}
/**
- * Instantiates the model as specified by the action and passed to this method. The model name specified
- * may either be a single string with the model name, or an array structure where the main model
- * contains other models.
+ * Instantiates the model as specified by the action and passed to this method. The model
+ * name specified may either be a single string with the model name, or an array structure
+ * where the main model contains other models.
*
- * @param array $structure Model name (along with any inner model structure) to instantiate.
+ * @param array $structure Model name (along with any inner model structure) to
+ * instantiate.
* @return object
*/
private function instantiateModel ($structure) {
@@ -96,7 +110,10 @@
$model = new $structure();
}
else if (is_array($structure)) {
- // With an array structure for models, the first array element must be the main model class
+ /*
+ * With an array structure for models, the first array element must be the main
+ * model class.
+ */
$mainModel = array_shift($structure);
$model = new $mainModel();
@@ -122,17 +139,16 @@
}
else return null;
- $model->isDirty = true;
return $model;
}
/**
- * Populates a specific property of a model with a value. The property is a <em>property path</em>
- * of the form <code>outerProperty::innerProperty</code> in which case <code>outerProperty</code> in
- * the model must be another model with the an <code>innerProperty</code> property to be populated
- * with the value.
+ * Populates a specific property of a model with a value. The property is a
+ * <em>property path</em> of the form <code>outerProperty::innerProperty</code> in which
+ * case <code>outerProperty</code> in the model must be another model with the an
+ * <code>innerProperty</code> property to be populated with the value.
*
- * TODO: This must be better documented and possibly add property_exists()-checks
+ * TODO: This must be better documented internally.
*
* @param object $model Model object to populate.
* @param string $property Property to populate.
@@ -164,15 +180,25 @@
}
break;
}
+
+ $this->setProperty($model, $currentProp, $value);
}
-
- $model->$currentProp = $this->convertType($value);
- }
- }
-
- /**
- * Converts textual values from the input to actual PHP types. Currently booleans and empty strings to
- * nulls are implemented.
+ }
+ }
+
+ /**
+ * Sets a property value on the model, if the value is different from the current value.
+ */
+ private function setProperty ($model, $property, $value) {
+ if ($model->$property !== $value) {
+ $model->$property = $value;
+ $model->isDirty = true;
+ }
+ }
+
+ /**
+ * Converts textual values from the input to actual PHP types. Currently booleans and empty
+ * strings to nulls are implemented.
*
* @param string $value Value from input.
* @return mixed Converted value.
=== modified file 'src/models/ModelProxy.php'
--- src/models/ModelProxy.php 2009-04-12 01:09:55 +0000
+++ src/models/ModelProxy.php 2009-05-18 15:15:52 +0000
@@ -184,7 +184,7 @@
if (property_exists($model, $name)) {
if ($model->$name !== $value) {
$model->$name = $value;
- $this->modelChanged($model);
+ $this->modelChanged($model, $value);
}
}
}
@@ -238,7 +238,7 @@
if (is_object($value)) {
if ($offset !== null) {
$this->models[$offset] = $value;
- $this->modelChanged($this->models[$offset]);
+ $this->modelChanged($this->models[$offset], $value);
}
else {
$this->models[] = $value;
@@ -315,10 +315,17 @@
/**
* Flag the model as dirty, as changes have been made to its state.
*
- * @param object $model The model whose state has changed.
+ * @param object $model The model whose state has changed.
+ * @param mixed $newValue Optional value that was set on a property of the model. If
+ * <code>NULL</code>, an object or an array we assume proxifying
+ * inner models may be required.
*/
- protected function modelChanged ($model) {
+ protected function modelChanged ($model, $newValue = null) {
$model->isDirty = true;
+
+ if ($newValue === null || is_array($newValue) || is_object($newValue)) {
+ $this->isInnerProxied = false;
+ }
}
/**
@@ -339,9 +346,9 @@
}
/**
- * Saves the supplied model by calling the <code>update()</code> DAO method if the model isn't
- * new and changes have been made on its data, or the <code>insert()</code> DAO method if it's
- * new.
+ * Saves the supplied model by calling the <code>update()</code> DAO method if the model
+ * isn't new and changes have been made on its data, or the <code>insert()</code> DAO
+ * method if it's new.
*
* @param object $model The model to save.
* @return int Number of rows affected in the database.
@@ -350,7 +357,14 @@
$numAffected = 0;
if (!empty($model->original)) {
- if (property_exists($model, 'isDirty') && $model->isDirty) {
+ /*
+ * We assume the model is dirty unless explicitly flagged as non-dirty. This is a
+ * change from earlier Kolibri snapshots where we assumed it was NOT dirty without
+ * the flag. This was changed based on the fact that we sometimes instantiate our
+ * own plain models, whereby we had to manually set isDirty = true if we were
+ * editing, which is not something the user should be concerned about.
+ */
+ if (!property_exists($model, 'isDirty') || $model->isDirty) {
$numAffected = $this->objects->update($model);
}
}
=== modified file 'src/models/Models.php'
--- src/models/Models.php 2008-12-03 02:22:07 +0000
+++ src/models/Models.php 2009-05-13 20:22:32 +0000
@@ -42,8 +42,8 @@
return new ModelProxy($model);
}
- // Unsupported object type, simply return it
- return $model;
+ // Unsupported object type, return null
+ return null;
}
}
?>
=== modified file 'src/models/ValidateableModelProxy.php'
--- src/models/ValidateableModelProxy.php 2009-04-04 18:03:16 +0000
+++ src/models/ValidateableModelProxy.php 2009-05-13 20:22:32 +0000
@@ -4,12 +4,12 @@
* models in order to add support for validation.
*/
class ValidateableModelProxy extends ModelProxy {
-
+
/**
* @var Validator
*/
private $validator;
-
+
/**
* Creates a <code>ValidateableModelProxy</code> instance for the model supplied. It is assumed
* that the model has been verified <code>Validateable</code>.
@@ -19,15 +19,15 @@
public function __construct ($model) {
parent::__construct($model);
}
-
+
/**
* Overrides ModelProxy::save() by making sure contained models are valid before they
* are saved. Contained models that have already been validated are not validated again.
- *
+ *
* @return mixed Number of saved rows in the database, or <code>false</code> if a
* a model contains invalid data or a preSave() method on a model returned
* false.
- *
+ *
*/
public function save () {
if (!$this->validate()) {
@@ -35,18 +35,18 @@
}
return parent::save();
}
-
+
/**
* Validates the contained models and returns <code>true</code> if all are valid or
* <code>false</code> if one or more are invalid.
- *
+ *
* @return bool <code>true</code> if all models are valid, <code>false</code> if not.
*/
public function validate () {
$this->proxifyInnerModels();
$this->initValidator();
$isValid = true; // We start out with valid state
-
+
foreach ($this->models as $model) {
// And set invalid for invalid objects, but never again valid
$isValid = ($this->validateModel($model) ? $isValid : false);
@@ -57,23 +57,23 @@
}
}
}
-
+
return $isValid;
}
-
+
/**
* Alias of validate() to accommodate for more readable code (i.e. for tests).
- *
+ *
* @return bool
*/
public function isValid () {
return $this->validate();
}
-
+
/**
* Validates the supplied model. If the model has already been validated (and is unchanged
* since) its previous result is returned, else the Validator is invoked to validate the model.
- *
+ *
* @param object $model The model to validate.
* @return bool
*/
@@ -83,18 +83,21 @@
}
return $this->validator->validate($model);
}
-
+
/**
* Remove validated flag, as changes have been made to its state and it's unknown whether it
* is valid or not.
*
- * @param object $model The model whose state has changed.
+ * @param object $model The model whose state has changed.
+ * @param mixed $newValue Optional value that was set on a property of the model. If
+ * <code>NULL</code>, an object or an array we assume proxifying
+ * inner models may be required.
*/
- protected function modelChanged ($model) {
- parent::modelChanged($model);
+ protected function modelChanged ($model, $newValue = null) {
+ parent::modelChanged($model, $newValue);
unset($model->isValid);
}
-
+
/**
* Initialized the validator if not already initialized.
*/
=== modified file 'src/specs/KolibriContext.php'
--- src/specs/KolibriContext.php 2009-04-29 10:37:01 +0000
+++ src/specs/KolibriContext.php 2009-06-02 12:11:36 +0000
@@ -1,81 +1,131 @@
<?php
/**
- * This class is the Kolibri Test framework. It serves as a class for Model, Action and View
- * testing. Right now it support Action and Model testing. You have to extend KolibriContext
- * to use this test-framework. It reflects the same methods as PHPSpec_Context has, but they
- * are named differently. The corresponding method names are setup(), preSpec(), postSpec()
- * and tearDown().
+ * This class is the Kolibri Test framework.
+ *
+ * It serves as a class for model, action and integration testing. To use this test framework,
+ * simply extend KolibriContext and include SpecHelper. This class reflects the same methods
+ * as PHPSPec_Context, but they are named differently. The corresponding method names are
+ * setUp(), preSpec(), postSpec() and tearDown().
*/
class KolibriContext extends PHPSpec_Context {
/**
* Holds the fixtures loaded, only populated during model testing.
* @var Fixtures
*/
- public $fixtures;
+ protected $fixtures;
/**
* Name of the model we are testing, if we are model testing.
* @var string
*/
- public $modelName;
+ protected $modelName;
+
+ /**
+ * The browser through Selenium during integration testing.
+ * @var Testing_Selenium
+ */
+ protected $browser;
/**
* A reference to the database connection.
* @var DatabaseConnection
*/
- private $db;
+ protected $db;
/**
* The current test type, corresponding to one of the class constants.
* @var string
*/
- private $testType;
-
- const ACTION_TEST = 'action';
- const VIEW_TEST = 'view';
- const MODEL_TEST = 'model';
-
- /**
- * Executes before all spec methods are invoked, and triggers the setup() method if
- * present. Distinguishes between model, action and view testing. It also establishes a
- * database connection which is used to roll back any changes between each spec.
- */
- public function beforeAll () {
+ protected $testType;
+
+ // Constants used to identify type of testing
+ const ACTION_TEST = 'action';
+ const INTEGRATION_TEST = 'integration';
+ const MODEL_TEST = 'model';
+
+ /**
+ * Internal initialization method which figures out what type of testing is currently being
+ * done; either model, action or integration testing. It also establishes a database
+ * connection which is used to begin/rollback any changes between each spec.
+ */
+ protected function init () {
if (Config::getMode() != Config::TEST) {
throw new Exception('KolibriTestCase requires that the current KOLIBRI_MODE is set
to TEST.');
}
-
+
+ // Figure out the filename of the current class
$className = get_class($this);
+ $reflection = new ReflectionClass($className);
+ $classFilename = $reflection->getFileName();
- if (substr(strtolower($className), -5) == self::MODEL_TEST) {
+ // Figure out which "testing type directory" we are within
+ do {
+ $currentPath = (isset($currentPath) ? $currentPath . '/..' : $classFilename);
+ $mainDir = basename(dirname(realpath($currentPath)));
+ $parentDir = basename(dirname(realpath($currentPath . '/..')));
+ } while (
+ $parentDir !== 'specs' &&
+ $parentDir !== 'actions' &&
+ $parentDir !== 'models' &&
+ $parentDir !== 'integration'
+ );
+
+ // If class name contains 'model' or we are within the models dir; model testing
+ if (($inName = substr(strtolower($className), -5)) == self::MODEL_TEST
+ || $parentDir == 'models') {
$this->testType = self::MODEL_TEST;
- $this->modelName = substr($className, 8, -5);
- $this->fixtures = new Fixtures($this->modelName);
+ if ($inName) {
+ $this->modelName = substr($className, 8, -5);
+ }
+ else $this->modelName = ucfirst($mainDir);
}
- elseif (substr(strtolower($className), -6) == self::ACTION_TEST) {
+ // Else if class name contains 'action' or we are within actions dir; action testing
+ else if ($mainDir == 'actions'
+ || $parentDir == 'actions'
+ || substr(strtolower($className), -6) == self::ACTION_TEST) {
$this->testType = self::ACTION_TEST;
}
- elseif (substr(strtolower($className), -4) == self::VIEW_TEST) {
- $this->testType = self::VIEW_TEST;
- throw new Exception('KolibriContext does not support view testing yet.');
+ // Else if we are within the integration dir; integration testing
+ else if ($mainDir == 'integration' || $parentDir == 'integration') {
+ $this->testType = self::INTEGRATION_TEST;
+ $this->browser = self::getBrowserInstance();
+
+ /*
+ * We register a shutdown function to stop the browser after the very last
+ * test -- we don't want to start/stop the browser all the time.
+ */
+ register_shutdown_function(array('KolibriContext', 'stopBrowserInstance'));
}
else {
- throw new Exception('KolibriTestCase needs to have either Model, Action or View
- in the end of the class name.');
+ throw new Exception('KolibriContext classes must be within one of the directories
+ specs/actions, specs/integration or specs/models.');
}
-
+
$this->db = DatabaseFactory::getConnection();
- if (method_exists($this, 'setup')) {
- $this->setup();
+ }
+
+ /**
+ * Executes before all spec methods are invoked, and triggers the setUp() method if
+ * present.
+ */
+ public function beforeAll () {
+ $this->init();
+ if (method_exists($this, 'setUp')) {
+ $this->setUp();
}
}
-
+
/**
- * Triggers the preSpec() method for doing something _before_ a spec has been invoked.
+ * Starts a new database transaction before each spec and refreshes any fixtures for
+ * models specs. Also triggers a preSpec() method, if defined, for doing something
+ * _before_ a spec has been invoked.
*/
public function before () {
$this->db->begin();
+ if ($this->testType == self::MODEL_TEST) {
+ $this->fixtures = new Fixtures($this->modelName);
+ }
if (method_exists($this, 'preSpec')) {
$this->preSpec();
}
@@ -91,9 +141,9 @@
}
$this->db->rollback();
}
-
+
/**
- * Triggers the tearDown() method for doing something _after all_ specs has runned.
+ * Triggers the tearDown() method for doing something _after all_ specs has runned.
*/
public function afterAll () {
if (method_exists($this, 'tearDown')) {
@@ -112,7 +162,7 @@
ob_flush();
}
}
-
+
/**
* Fires a GET request for testing an action. Any supplied parameteres and session
* data are passed along to the request. This provides access to the <code>$request</code>,
@@ -161,7 +211,7 @@
$_SERVER['REQUEST_URI'] = $uri;
$_SESSION = ($session !== null ? $session : array());
}
-
+
/**
* Does not allow you to use post and/or get in any other testing classes than action.
*
@@ -176,5 +226,29 @@
return true;
}
+ /**
+ * Returns a Selenium browser instance, creating one if it doesn't already exist (unless
+ * $createAnyway is false).
+ */
+ private static function getBrowserInstance ($createAnyway = true) {
+ static $browser;
+ if (!isset($browser) && $createAnyway) {
+ require('Testing/Selenium.php');
+ $browser = new Testing_Selenium("*firefox", Config::get('webRoot'));
+ $browser->start();
+ }
+ return $browser;
+ }
+
+ /**
+ * Stops the current browser instance if present. Should not be called explicitly; it will
+ * be called implicitly at the very end of script execution.
+ */
+ public static function stopBrowserInstance () {
+ $browser = self::getBrowserInstance(false);
+ if ($browser !== null) {
+ $browser->stop();
+ }
+ }
}
?>
=== modified file 'src/validation/ExistsValidator.php'
--- src/validation/ExistsValidator.php 2008-10-20 16:41:10 +0000
+++ src/validation/ExistsValidator.php 2009-05-13 20:22:32 +0000
@@ -8,7 +8,8 @@
public function validate ($property, $rules) {
if (!isset($rules['condition'])) {
- if ($this->model->$property === null || $this->model->$property == '') {
+ if ($this->model->$property === null || $this->model->$property === ''
+ || $this->model->$property === array()) {
return array('exists' => $rules['name']);
}
}
Follow ups