diff --git a/alloy/Module/Generator/Controller.php b/alloy/Module/Generator/Controller.php new file mode 100644 index 0000000..734d619 --- /dev/null +++ b/alloy/Module/Generator/Controller.php @@ -0,0 +1,134 @@ +request()->isCli()) { + throw new Alloy\Exception\FileNotFound("Requested file or page not found. Please check the URL and try again."); + } + } + + + /** + * Scaffold module + */ + public function scaffoldAction(Request $request) + { + $kernel = \Kernel(); + + /** + * Variables we expect in CLI request: + * + * $name string Name of module + */ + $name = preg_replace('/[^a-zA-Z0-9_ ]/', '', $kernel->formatUnderscoreWord($request->name)); + $name_table = preg_replace('/\s+/', '_', strtolower($name)); + + // URL-usable name + $name_url = preg_replace('/\s+/', '_', $name); + + // Directory path + $name_dir = preg_replace('/\s+/', '/', $name); + + // Valid PHP namespace + $namespace = preg_replace('/\s+/', '\\', $name); + + // TODO: Make this dynamic and generated (allow user field definitions) + $fields = array( + 'name' => array('type' => 'string', 'required' => true) + ); + $field_string = ""; + foreach($fields as $fieldName => $fieldInfo) { + // Flattens field definitions for writing in Entity.php file + // str_replace calls to remove some of the prettyprinting and odd formats var_export does by default + $field_string .= "'" . $fieldName . "' => " . str_replace( + array("array (", "\n", ",)"), + array("array(", "", ")"), + var_export($fieldInfo, true) + ) . ",\n"; + } + + // Set tag variables + $generatorTagNames = compact('name', 'name_table', 'name_sanitized', 'name_url', 'name_dir', 'namespace', 'fields', 'field_string'); + + echo PHP_EOL; + + // File paths + $scaffoldPath = __DIR__ . '/scaffold/'; + $modulePath = $kernel->config('app.path.root') .'/Module/' . $name_dir . '/'; + + // Output + echo 'Generator Module Info' . PHP_EOL; + echo '-----------------------------------------------------------------------' . PHP_EOL; + echo 'Name = ' . $name . PHP_EOL; + echo 'Namespace = ' . $namespace . PHP_EOL; + echo 'Datasource (Table) = ' . $name_table . PHP_EOL; + echo 'Path = ' . $modulePath . PHP_EOL; + echo '-----------------------------------------------------------------------' . PHP_EOL; + echo PHP_EOL; + + // Variables (in 'generator' namespace): + // * name + // * name_table + // * $fields + // + // Other variables + // * $fields (from Entity::fields()) + + // Build tag replecement set for scaffold-generated files + $generator_tag_start = '{$generator.'; + $generator_tag_end = '}'; + $generatorTags = array(); + foreach($generatorTagNames as $tag => $val) { + if(is_array($val)) { + $val = var_export($val, true); + } + $generatorTags[$generator_tag_start . $tag . $generator_tag_end] = $val; + } + + // Copy files and replace tokens + $scaffoldFiles = array( + 'Controller.php', + 'Entity.php', + 'views/indexAction.html.php', + 'views/newAction.html.php', + 'views/editAction.html.php', + 'views/deleteAction.html.php', + 'views/viewAction.html.php' + ); + foreach($scaffoldFiles as $sFile) { + $tmpl = file_get_contents($scaffoldPath . $sFile); + + // Replace template tags + $tmpl = str_replace(array_keys($generatorTags), array_values($generatorTags), $tmpl); + + // Ensure destination directory exists + $sfDir = dirname($modulePath . $sFile); + if(!is_dir($sfDir)) { + mkdir($sfDir, 0755, true); + } + + // Write file to destination module directory + $result = file_put_contents($modulePath . $sFile, $tmpl); + if($result) { + echo "+ Generated '" . $sFile . "'"; + } else { + echo "[ERROR] Unable to generate '" . $sFile . "'"; + } + echo PHP_EOL; + } + + echo PHP_EOL; + } +} \ No newline at end of file diff --git a/alloy/Module/Generator/scaffold/Controller.php b/alloy/Module/Generator/scaffold/Controller.php new file mode 100644 index 0000000..e09120d --- /dev/null +++ b/alloy/Module/Generator/scaffold/Controller.php @@ -0,0 +1,241 @@ +mapper()->all(self::ENTITY); + $fields = $kernel->mapper()->entityManager()->fields(self::ENTITY); + + // Return 404 if no items + if(!$items) { + return false; + } + + // HTML template + if('html' == $request->format) { + return $this->template(__FUNCTION__) + ->set(compact('items', 'fields')); + } + // Resource object (JSON/XML, etc) + return $kernel->resource($items); + } + + + /** + * View single item + * @method GET + */ + public function viewAction(Request $request) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + + $item = $mapper->get(self::ENTITY, (int) $request->item); + if(!$item) { + return false; + } + + if('html' == $request->format) { + return $this->template(__FUNCTION__) + ->set(compact('item')); + } else { + return $kernel->resource($item); + } + } + + + /** + * Create new item form + * @method GET + */ + public function newAction(Request $request) + { + $kernel = \Kernel(); + $item = $kernel->mapper()->get(self::ENTITY); + + return $this->template(__FUNCTION__) + ->set(array( + 'item' => $item, + 'form' => $this->formView($item)->data($request->query()) + )); + } + + + /** + * Helper to save item + */ + public function saveItem(Entity $item) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + $request = $kernel->request(); + + // Attempt save + if($mapper->save($item)) { + $itemUrl = $kernel->url(array('module' => '{$generator.name_url}', 'item' => $item->id), 'module_item'); + + // HTML + if('html' == $request->format) { + return $kernel->redirect($itemUrl); + // Others (XML, JSON) + } else { + return $kernel->resource($item->data()) + ->created($itemUrl); + } + // Handle errors + } else { + // HTML + if('html' == $request->format) { + // Re-display form + $res = $kernel->spotForm($item); + // Others (XML, JSON) + } else { + $res = $kernel->resource(); + } + + // Set HTTP status and errors + return $res->status(400) + ->errors($item->errors()); + } + } + + + /** + * New item creation + * @method POST + */ + public function postMethod(Request $request) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + + $item = $mapper->get(self::ENTITY); + $item->data($request->post()); + + // Common save functionality + return $this->saveItem($item); + } + + + /** + * Edit form for item + * @method GET + */ + public function editAction(Request $request) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + + $item = $mapper->get(self::ENTITY, (int) $request->item); + if(!$item) { + return false; + } + + return $this->template(__FUNCTION__) + ->set(array( + 'item' => $item, + 'form' => $this->formView($item) + )); + } + + + /** + * Edit existing item + * @method PUT + */ + public function putMethod(Request $request) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + + $item = $mapper->get(self::ENTITY, (int) $request->item); + if(!$item) { + return false; + } + + // Set all POST data that can be set + $item->data($request->post()); + + // Ensure 'id' cannot be modified + $item->id = (int) $request->item; + + // Update 'last modified' date + $item->date_modified = new \DateTime(); + + // Common save functionality + return $this->saveItem($item); + } + + + /** + * Delete confirmation + * @method GET + */ + public function deleteAction(Request $request) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + + $item = $mapper->get(self::ENTITY, (int) $request->item); + if(!$item) { + return false; + } + + return $this->template(__FUNCTION__) + ->set(compact('item')); + } + + + /** + * Delete post + * @method DELETE + */ + public function deleteMethod(Request $request) + { + $kernel = \Kernel(); + $mapper = $kernel->mapper(); + + $item = $mapper->get(self::ENTITY, (int) $request->item); + if(!$item) { + return false; + } + + $mapper->delete($item); + + return $kernel->redirect($kernel->url(array('module' => '{$generator.name_url}'), 'module')); + } + + + /** + * Entity form with Alloy form generic + */ + protected function formView($entity = null) + { + return \Kernel()->spotForm($entity); + } + + + /** + * Automatic install/migrate method + */ + public function install() + { + $mapper = \Kernel()->mapper(); + $mapper->migrate(self::ENTITY); + } +} \ No newline at end of file diff --git a/alloy/Module/Generator/scaffold/Entity.php b/alloy/Module/Generator/scaffold/Entity.php new file mode 100644 index 0000000..83ee402 --- /dev/null +++ b/alloy/Module/Generator/scaffold/Entity.php @@ -0,0 +1,18 @@ + array('type' => 'int', 'primary' => true, 'serial' => true), + {$generator.field_string} + 'date_created' => array('type' => 'datetime', 'default' => new \DateTime()), + 'date_modified' => array('type' => 'datetime', 'default' => new \DateTime()) + ); + } +} \ No newline at end of file diff --git a/alloy/Module/Generator/scaffold/views/deleteAction.html.php b/alloy/Module/Generator/scaffold/views/deleteAction.html.php new file mode 100644 index 0000000..5a5cf14 --- /dev/null +++ b/alloy/Module/Generator/scaffold/views/deleteAction.html.php @@ -0,0 +1,13 @@ +head()->title('Delete Item'); ?> + +
Really delete this item?
+ +generic('Form'); + +$form->action($kernel->url(array('module' => '{$generator.name_url}', 'item' => $item->id), 'module_item')) + ->method('delete') + ->submit('Delete'); +echo $form->content(); +?> \ No newline at end of file diff --git a/alloy/Module/Generator/scaffold/views/editAction.html.php b/alloy/Module/Generator/scaffold/views/editAction.html.php new file mode 100644 index 0000000..28687f3 --- /dev/null +++ b/alloy/Module/Generator/scaffold/views/editAction.html.php @@ -0,0 +1,8 @@ +head()->title('Edit Item'); ?> + +action($kernel->url(array('module' => '{$generator.name_url}', 'item' => $item->id), 'module_item')) + ->method('put'); +echo $form->content(); +?> \ No newline at end of file diff --git a/alloy/Module/Generator/scaffold/views/indexAction.html.php b/alloy/Module/Generator/scaffold/views/indexAction.html.php new file mode 100644 index 0000000..4420e66 --- /dev/null +++ b/alloy/Module/Generator/scaffold/views/indexAction.html.php @@ -0,0 +1,32 @@ +head()->title('Listing Items'); ?> + + + +generic('datagrid'); + +// Set data collection to use for rows +$table->data($items); + +// Add each column heading and cell output callback +foreach($fields as $field => $info) { + $table->column($kernel->formatUnderscoreWord($field), function($item) use($field, $info) { + return $item->$field; + }); +} + +// Edit/delete links +$table->column('View', function($item) use($view) { + return $view->link('View', array('module' => '{$generator.name_url}', 'item' => $item->id), 'module_item'); +}); +$table->column('Edit', function($item) use($view) { + return $view->link('Edit', array('module' => '{$generator.name_url}', 'item' => $item->id, 'action' => 'edit'), 'module_item_action'); +}); +$table->column('Delete', function($item) use($view) { + return $view->link('Delete', array('module' => '{$generator.name_url}', 'item' => $item->id, 'action' => 'delete'), 'module_item_action'); +}); + +// Output table +echo $table->content(); +?> diff --git a/alloy/Module/Generator/scaffold/views/newAction.html.php b/alloy/Module/Generator/scaffold/views/newAction.html.php new file mode 100644 index 0000000..098b151 --- /dev/null +++ b/alloy/Module/Generator/scaffold/views/newAction.html.php @@ -0,0 +1,8 @@ +head()->title('New Item'); ?> + +action($kernel->url(array('module' => '{$generator.name_url}'), 'module')) + ->method('post'); +echo $form->content(); +?> \ No newline at end of file diff --git a/alloy/Module/Generator/scaffold/views/viewAction.html.php b/alloy/Module/Generator/scaffold/views/viewAction.html.php new file mode 100644 index 0000000..7cd1298 --- /dev/null +++ b/alloy/Module/Generator/scaffold/views/viewAction.html.php @@ -0,0 +1,9 @@ +head()->title('View Item'); ?> + +data() as $field => $value): ?> ++ : +
+ + +link('< Listing', array('module' => '{$generator.name_url}'), 'module', array('class' => 'btn')); ?> \ No newline at end of file diff --git a/alloy/Plugin/Spot/Plugin.php b/alloy/Plugin/Spot/Plugin.php index a86554a..2bd54ee 100644 --- a/alloy/Plugin/Spot/Plugin.php +++ b/alloy/Plugin/Spot/Plugin.php @@ -134,8 +134,14 @@ public function autoinstallOnException($content) if($content instanceof \Spot\Exception_Datasource_Missing ||'42S02' == $content->getCode() || false !== stripos($content->getMessage(), 'Base table or view not found')) { - // Table not found - auto-install module to cause Entity migrations + // Last dispatch attempt $ld = $kernel->lastDispatch(); + + // Debug trace message + $mName = is_object($ld['module']) ? get_class($ld['module']) : $ld['module']; + $kernel->trace("PDO Exception on module '" . $mName . "' when dispatching '" . $ld['action'] . "' Attempting auto-install in Spot plugin at " . __METHOD__ . "", $content); + + // Table not found - auto-install module to cause Entity migrations $content = $kernel->dispatch($ld['module'], 'install'); } } diff --git a/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterAbstract.php b/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterAbstract.php index d555d67..f3ea4a2 100644 --- a/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterAbstract.php +++ b/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterAbstract.php @@ -150,17 +150,17 @@ protected function dateTimeObject($format) * * The format of the supplied DSN is in its fullest form: *
- * adapter(dbsyntax)://username:password@protocol+hostspec/database?option=8&another=true
+ * adapter(dbsyntax)://username:password@protocol+host/database?option=8&another=true
*
*
* Most variations are allowed:
*
- * adapter://username:password@protocol+hostspec:110//usr/db_file.db?mode=0644
- * adapter://username:password@hostspec/database_name
- * adapter://username:password@hostspec
- * adapter://username@hostspec
- * adapter://hostspec/database
- * adapter://hostspec
+ * adapter://username:password@protocol+host:110//usr/db_file.db?mode=0644
+ * adapter://username:password@host/database_name
+ * adapter://username:password@host
+ * adapter://username@host
+ * adapter://host/database
+ * adapter://host
* adapter(dbsyntax)
* adapter
*
@@ -173,7 +173,7 @@ protected function dateTimeObject($format)
* + adapter: Database backend used in PHP (mysql, odbc etc.)
* + dbsyntax: Database used with regards to SQL syntax etc.
* + protocol: Communication protocol to use (tcp, unix etc.)
- * + hostspec: Host specification (hostname[:port])
+ * + host: Host specification (hostname[:port])
* + database: Database to use on the DBMS server
* + username: User name for login
* + password: Password for login
@@ -191,7 +191,7 @@ public static function parseDSN( $dsn )
'username' => FALSE,
'password' => FALSE,
'protocol' => FALSE,
- 'hostspec' => FALSE,
+ 'host' => FALSE,
'port' => FALSE,
'socket' => FALSE,
'database' => FALSE,
@@ -238,7 +238,7 @@ public static function parseDSN( $dsn )
}
// Get (if found): username and password
- // $dsn => username:password@protocol+hostspec/database
+ // $dsn => username:password@protocol+host/database
if ( ( $at = strrpos( (string) $dsn, '@' ) ) !== FALSE )
{
$str = substr( $dsn, 0, $at );
@@ -254,7 +254,7 @@ public static function parseDSN( $dsn )
}
}
- // Find protocol and hostspec
+ // Find protocol and host
if ( preg_match( '|^([^(]+)\((.*?)\)/?(.*?)$|', $dsn, $match ) )
{
@@ -265,7 +265,7 @@ public static function parseDSN( $dsn )
}
else
{
- // $dsn => protocol+hostspec/database (old format)
+ // $dsn => protocol+host/database (old format)
if ( strpos( $dsn, '+' ) !== FALSE )
{
list( $proto, $dsn ) = explode( '+', $dsn, 2 );
@@ -288,11 +288,11 @@ public static function parseDSN( $dsn )
{
if ( strpos( $proto_opts, ':' ) !== FALSE )
{
- list( $parsed['hostspec'], $parsed['port'] ) = explode( ':', $proto_opts );
+ list( $parsed['host'], $parsed['port'] ) = explode( ':', $proto_opts );
}
else
{
- $parsed['hostspec'] = $proto_opts;
+ $parsed['host'] = $proto_opts;
}
}
elseif ( $parsed['protocol'] == 'unix' )
diff --git a/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterInterface.php b/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterInterface.php
index ca8f21f..4e04810 100644
--- a/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterInterface.php
+++ b/alloy/Plugin/Spot/lib/Spot/Adapter/AdapterInterface.php
@@ -79,6 +79,12 @@ public function create($source, array $data, array $options = array());
public function read(\Spot\Query $query, array $options = array());
+ /*
+ * Count number of rows in source based on conditions
+ */
+ public function count(\Spot\Query $query, array $options = array());
+
+
/**
* Update entity
*/
@@ -117,4 +123,4 @@ public function createDatabase($database);
* Will throw errors if user does not have proper permissions
*/
public function dropDatabase($database);
-}
+}
\ No newline at end of file
diff --git a/alloy/Plugin/Spot/lib/Spot/Adapter/Mongodb.php b/alloy/Plugin/Spot/lib/Spot/Adapter/Mongodb.php
index f82647d..b661f4d 100644
--- a/alloy/Plugin/Spot/lib/Spot/Adapter/Mongodb.php
+++ b/alloy/Plugin/Spot/lib/Spot/Adapter/Mongodb.php
@@ -206,6 +206,24 @@ public function read(\Spot\Query $query, array $options = array())
return $this->toCollection($query, $cursor);
}
+ /*
+ * Count number of rows in source based on criteria
+ */
+ public function count(\Spot\Query $query, array $options = array())
+ {
+ // Load criteria
+ $criteria = $this->queryConditions($query);
+
+ //find and return count
+ $count = $this->mongoCollection($query->datasource)->find($criteria)->count();
+
+ // Add query to log
+ Spot_Log::addQuery($this, $criteria);
+
+ // Return count
+ return is_numeric($count) ? (int)$count : 0;
+ }
+
/**
* Update entity
*/
@@ -437,4 +455,4 @@ public function mongoCollection($collectionName)
{
return $this->mongoDatabase()->$collectionName;
}
-}
+}
\ No newline at end of file
diff --git a/alloy/Plugin/Spot/lib/Spot/Adapter/Mysql.php b/alloy/Plugin/Spot/lib/Spot/Adapter/Mysql.php
index 6c0226a..309b4b2 100644
--- a/alloy/Plugin/Spot/lib/Spot/Adapter/Mysql.php
+++ b/alloy/Plugin/Spot/lib/Spot/Adapter/Mysql.php
@@ -128,12 +128,14 @@ public function migrateSyntaxFieldCreate($fieldName, array $fieldInfo)
if(!isset($this->_fieldTypeMap[$fieldInfo['type']])) {
throw new \Spot\Exception("Field type '" . $fieldInfo['type'] . "' not supported");
}
-
- $fieldInfo = array_merge($fieldInfo, $this->_fieldTypeMap[$fieldInfo['type']]);
+ //Ensure this class will choose adapter type
+ unset($fieldInfo['adapter_type']);
+
+ $fieldInfo = array_merge($this->_fieldTypeMap[$fieldInfo['type']],$fieldInfo);
$syntax = "`" . $fieldName . "` " . $fieldInfo['adapter_type'];
// Column type and length
- $syntax .= ($fieldInfo['length']) ? '(' . $fieldInfo['length'] . ')' : '';
+ $syntax .= is_int($fieldInfo['length']) ? '(' . $fieldInfo['length'] . ')' : '';
// Unsigned
$syntax .= ($fieldInfo['unsigned']) ? ' unsigned' : '';
// Collate
@@ -355,7 +357,7 @@ public function migrateSyntaxTableUpdate($table, array $formattedFields, array $
}
// Extra
- $syntax .= "\n) ENGINE=" . $options['engine'] . " DEFAULT CHARSET=" . $options['charset'] . " COLLATE=" . $options['collate'] . ";";
+ $syntax .= ",\n ENGINE=" . $options['engine'] . " DEFAULT CHARSET=" . $options['charset'] . " COLLATE=" . $options['collate'] . ";";
return $syntax;
}
diff --git a/alloy/Plugin/Spot/lib/Spot/Adapter/PDO/Abstract.php b/alloy/Plugin/Spot/lib/Spot/Adapter/PDO/Abstract.php
index a5f193e..d1dc04a 100644
--- a/alloy/Plugin/Spot/lib/Spot/Adapter/PDO/Abstract.php
+++ b/alloy/Plugin/Spot/lib/Spot/Adapter/PDO/Abstract.php
@@ -28,7 +28,7 @@ public function connection()
// Establish connection
try {
- $dsn = $dsnp['adapter'].':host='.$dsnp['hostspec'].';dbname='.$dsnp['database'];
+ $dsn = $dsnp['adapter'].':host='.$dsnp['host'].';dbname='.$dsnp['database'];
$this->_connection = new \PDO($dsn, $dsnp['username'], $dsnp['password'], $this->_options);
// Throw exceptions by default
$this->_connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
@@ -180,11 +180,13 @@ public function migrateTableUpdate($table, array $formattedFields, array $option
// Run SQL
$this->connection()->exec($sql);
} catch(\PDOException $e) {
- // Table does not exist
+ // Table does not exist - special Exception case
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table '" . $table . "' does not exist");
}
- return false;
+
+ // Re-throw exception
+ throw $e;
}
}
return true;
@@ -248,7 +250,9 @@ public function create($datasource, array $data, array $options = array())
if($stmt) {
// Execute
if($stmt->execute($binds)) {
- $result = $this->connection()->lastInsertId();
+ // Use 'id' if PK exists, otherwise returns true
+ $id = $this->connection()->lastInsertId();
+ $result = $id ? $id : true;
} else {
$result = false;
}
@@ -260,7 +264,9 @@ public function create($datasource, array $data, array $options = array())
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $datasource . "' does not exist");
}
- return false;
+
+ // Re-throw exception
+ throw $e;
}
return $result;
@@ -320,17 +326,69 @@ public function read(\Spot\Query $query, array $options = array())
} else {
$result = false;
}
- } catch(PDOException $e) {
+ } catch(\PDOException $e) {
// Table does not exist
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $query->datasource . "' does not exist");
}
+
+ // Re-throw exception
throw $e;
}
return $result;
}
+ /*
+ * Count number of rows in source based on conditions
+ */
+ public function count(\Spot\Query $query, array $options = array())
+ {
+ $conditions = $this->statementConditions($query->conditions);
+ $binds = $this->statementBinds($query->params());
+ $sql = "
+ SELECT COUNT(*) as count
+ FROM " . $query->datasource . "
+ " . ($conditions ? 'WHERE ' . $conditions : '') . "
+ " . ($query->group ? 'GROUP BY ' . implode(', ', $query->group) : '');
+
+ // Unset any NULL values in binds (compared as "IS NULL" and "IS NOT NULL" in SQL instead)
+ if($binds && count($binds) > 0) {
+ foreach($binds as $field => $value) {
+ if(null === $value) {
+ unset($binds[$field]);
+ }
+ }
+ }
+
+ // Add query to log
+ \Spot\Log::addQuery($this, $sql,$binds);
+
+ $result = false;
+ try {
+ // Prepare count query
+ $stmt = $this->connection()->prepare($sql);
+
+ //if prepared, execute
+ if($stmt && $stmt->execute($binds)) {
+ //the count is returned in the first column
+ $result = (int) $stmt->fetchColumn();
+ } else {
+ $result = false;
+ }
+ } catch(\PDOException $e) {
+ // Table does not exist
+ if($e->getCode() == "42S02") {
+ throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $query->datasource . "' does not exist");
+ }
+
+ // Re-throw exception
+ throw $e;
+ }
+
+ return $result;
+ }
+
/**
* Update entity
*/
@@ -378,7 +436,9 @@ public function update($datasource, array $data, array $where = array(), array $
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $datasource . "' does not exist");
}
- return false;
+
+ // Re-throw exception
+ throw $e;
}
} else {
$result = false;
@@ -422,7 +482,9 @@ public function delete($datasource, array $data, array $options = array())
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $datasource . "' does not exist");
}
- return false;
+
+ // Re-throw exception
+ throw $e;
}
}
@@ -444,7 +506,9 @@ public function truncateDatasource($datasource) {
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $datasource . "' does not exist");
}
- return false;
+
+ // Re-throw exception
+ throw $e;
}
}
@@ -466,7 +530,9 @@ public function dropDatasource($datasource) {
if($e->getCode() == "42S02") {
throw new \Spot\Exception_Datasource_Missing("Table or datasource '" . $datasource . "' does not exist");
}
- return false;
+
+ // Re-throw exception
+ throw $e;
}
}
@@ -745,4 +811,4 @@ protected function bindValues($stmt, array $binds)
}
return true;
}
-}
+}
\ No newline at end of file
diff --git a/alloy/Plugin/Spot/lib/Spot/Config.php b/alloy/Plugin/Spot/lib/Spot/Config.php
index 6151a2c..6b1aabe 100644
--- a/alloy/Plugin/Spot/lib/Spot/Config.php
+++ b/alloy/Plugin/Spot/lib/Spot/Config.php
@@ -5,95 +5,109 @@
* @package Spot
* @link http://spot.os.ly
*/
-class Config
+class Config implements \Serializable
{
- protected $_defaultConnection;
- protected $_connections = array();
-
-
- /**
- * Add database connection
- *
- * @param string $name Unique name for the connection
- * @param string $dsn DSN string for this connection
- * @param array $options Array of key => value options for adapter
- * @param boolean $defaut Use this connection as the default? The first connection added is automatically set as the default, even if this flag is false.
- * @return Spot_Adapter_Interface Spot adapter instance
- * @throws Spot_Exception
- */
- public function addConnection($name, $dsn, array $options = array(), $default = false)
- {
- // Connection name must be unique
- if(isset($this->_connections[$name])) {
- throw new Exception("Connection for '" . $name . "' already exists. Connection name must be unique.");
- }
-
- $dsnp = \Spot\Adapter\AdapterAbstract::parseDSN($dsn);
- $adapterClass = "\\Spot\\Adapter\\" . ucfirst($dsnp['adapter']);
- $adapter = new $adapterClass($dsn, $options);
-
- // Set as default connection?
- if(true === $default || null === $this->_defaultConnection) {
- $this->_defaultConnection = $name;
- }
-
- // Store connection and return adapter instance
- $this->_connections[$name] = $adapter;
- return $adapter;
- }
-
-
- /**
- * Get connection by name
- *
- * @param string $name Unique name of the connection to be returned
- * @return Spot_Adapter_Interface Spot adapter instance
- * @throws Spot_Exception
- */
- public function connection($name = null)
- {
- if(null === $name) {
- return $this->defaultConnection();
- }
-
- // Connection name must be unique
- if(!isset($this->_connections[$name])) {
- return false;
- }
-
- return $this->_connections[$name];
- }
-
-
- /**
- * Get default connection
- *
- * @return Spot_Adapter_Interface Spot adapter instance
- * @throws Spot_Exception
- */
- public function defaultConnection()
- {
- return $this->_connections[$this->_defaultConnection];
- }
-
-
- /**
- * Class loader
- *
- * @param string $className Name of class to load
- */
- public static function loadClass($className)
- {
- $loaded = false;
-
- // Require Spot namespaced files by assumed folder structure (naming convention)
- if(false !== strpos($className, "Spot\\")) {
- $classFile = trim(str_replace("\\", "/", str_replace("_", "/", str_replace('Spot\\', '', $className))), '\\');
- $loaded = require_once(__DIR__ . "/" . $classFile . ".php");
- }
-
- return $loaded;
- }
+ protected $_defaultConnection;
+ protected $_connections = array();
+
+
+ /**
+ * Add database connection
+ *
+ * @param string $name Unique name for the connection
+ * @param string $dsn DSN string for this connection
+ * @param array $options Array of key => value options for adapter
+ * @param boolean $defaut Use this connection as the default? The first connection added is automatically set as the default, even if this flag is false.
+ * @return Spot_Adapter_Interface Spot adapter instance
+ * @throws Spot_Exception
+ */
+ public function addConnection($name, $dsn, array $options = array(), $default = false)
+ {
+ // Connection name must be unique
+ if(isset($this->_connections[$name])) {
+ throw new Exception("Connection for '" . $name . "' already exists. Connection name must be unique.");
+ }
+
+ $dsnp = \Spot\Adapter\AdapterAbstract::parseDSN($dsn);
+ $adapterClass = "\\Spot\\Adapter\\" . ucfirst($dsnp['adapter']);
+ $adapter = new $adapterClass($dsn, $options);
+
+ // Set as default connection?
+ if(true === $default || null === $this->_defaultConnection) {
+ $this->_defaultConnection = $name;
+ }
+
+ // Store connection and return adapter instance
+ $this->_connections[$name] = $adapter;
+ return $adapter;
+ }
+
+
+ /**
+ * Get connection by name
+ *
+ * @param string $name Unique name of the connection to be returned
+ * @return Spot_Adapter_Interface Spot adapter instance
+ * @throws Spot_Exception
+ */
+ public function connection($name = null)
+ {
+ if(null === $name) {
+ return $this->defaultConnection();
+ }
+
+ // Connection name must be unique
+ if(!isset($this->_connections[$name])) {
+ return false;
+ }
+
+ return $this->_connections[$name];
+ }
+
+
+ /**
+ * Get default connection
+ *
+ * @return Spot_Adapter_Interface Spot adapter instance
+ * @throws Spot_Exception
+ */
+ public function defaultConnection()
+ {
+ return $this->_connections[$this->_defaultConnection];
+ }
+
+
+ /**
+ * Class loader
+ *
+ * @param string $className Name of class to load
+ */
+ public static function loadClass($className)
+ {
+ $loaded = false;
+
+ // Require Spot namespaced files by assumed folder structure (naming convention)
+ if(false !== strpos($className, "Spot\\")) {
+ $classFile = trim(str_replace("\\", "/", str_replace("_", "/", str_replace('Spot\\', '', $className))), '\\');
+ $loaded = require_once(__DIR__ . "/" . $classFile . ".php");
+ }
+
+ return $loaded;
+ }
+
+
+ /**
+ * Default serialization behavior is to not attempt to serialize stored
+ * adapter connections at all (thanks @TheSavior re: Issue #7)
+ */
+ public function serialize()
+ {
+ return serialize(array());
+ }
+
+ public function unserialize($serialized)
+ {
+ }
}
diff --git a/alloy/Plugin/Spot/lib/Spot/Entity.php b/alloy/Plugin/Spot/lib/Spot/Entity.php
index 36c5322..1f39a39 100644
--- a/alloy/Plugin/Spot/lib/Spot/Entity.php
+++ b/alloy/Plugin/Spot/lib/Spot/Entity.php
@@ -17,6 +17,9 @@ abstract class Entity
protected $_data = array();
protected $_dataModified = array();
+ // Entity error messages (may be present after save attempt)
+ protected $_errors = array();
+
/**
* Constructor - allows setting of object properties with array on construct
@@ -62,10 +65,10 @@ public static function datasource($ds = null)
/**
* Datasource options getter/setter
*/
- public static function datasourceOptions($ds = null)
+ public static function datasourceOptions($dsOpts = null)
{
- if(null !== $ds) {
- static::$_datasourceOptions = $ds;
+ if(null !== $dsOpts) {
+ static::$_datasourceOptions = $dsOpts;
return $this;
}
return static::$_datasourceOptions;
@@ -156,6 +159,61 @@ public function toArray()
{
return $this->data();
}
+
+
+ /**
+ * Check if any errors exist
+ *
+ * @param string $field OPTIONAL field name
+ * @return boolean
+ */
+ public function hasErrors($field = null)
+ {
+ if(null !== $field) {
+ return isset($this->_errors[$field]) ? count($this->_errors[$field]) > 0 : false;
+ }
+ return count($this->_errors) > 0;
+ }
+
+
+ /**
+ * Error message getter/setter
+ *
+ * @param $field string|array String return errors with field key, array sets errors
+ * @return self|array|boolean Setter return self, getter returns array or boolean if key given and not found
+ */
+ public function errors($msgs = null)
+ {
+ // Return errors for given field
+ if(is_string($msgs)) {
+ return isset($this->_errors[$msgs]) ? $this->_errors[$msgs] : array();
+
+ // Set error messages from given array
+ } elseif(is_array($msgs)) {
+ $this->_errors = $msgs;
+ }
+ return $this->_errors;
+ }
+
+
+ /**
+ * Add an error to error messages array
+ *
+ * @param string $field Field name that error message relates to
+ * @param mixed $msg Error message text - String or array of messages
+ */
+ public function error($field, $msg)
+ {
+ if(is_array($msg)) {
+ // Add array of error messages about field
+ foreach($msg as $msgx) {
+ $this->_errors[$field][] = $msgx;
+ }
+ } else {
+ // Add to error array
+ $this->_errors[$field][] = $msg;
+ }
+ }
/**
diff --git a/alloy/Plugin/Spot/lib/Spot/Entity/Manager.php b/alloy/Plugin/Spot/lib/Spot/Entity/Manager.php
index 439e0dc..3ac15d0 100644
--- a/alloy/Plugin/Spot/lib/Spot/Entity/Manager.php
+++ b/alloy/Plugin/Spot/lib/Spot/Entity/Manager.php
@@ -110,7 +110,7 @@ public function fields($entityName)
}
// Format field will full set of default options
- if(isset($fieldInfo['type']) && isset($fieldTypeDefaults[$fieldOpts['type']])) {
+ if(isset($fieldOpts['type']) && isset($fieldTypeDefaults[$fieldOpts['type']])) {
// Include type defaults
$fieldOpts = array_merge($fieldDefaults, $fieldTypeDefaults[$fieldOpts['type']], $fieldOpts);
} else {
diff --git a/alloy/Plugin/Spot/lib/Spot/Mapper.php b/alloy/Plugin/Spot/lib/Spot/Mapper.php
index 01793e5..8cd3d4f 100644
--- a/alloy/Plugin/Spot/lib/Spot/Mapper.php
+++ b/alloy/Plugin/Spot/lib/Spot/Mapper.php
@@ -210,6 +210,12 @@ public function collection($entityName, $cursor)
$results = array();
$resultsIdentities = array();
+ // Ensure PDO only gives key => value pairs, not index-based fields as well
+ // Raw PDOStatement objects generally only come from running raw SQL queries or other custom stuff
+ if($cursor instanceof \PDOStatement) {
+ $cursor->setFetchMode(\PDO::FETCH_ASSOC);
+ }
+
// Fetch all results into new entity class
// @todo Move this to collection class so entities will be lazy-loaded by Collection iteration
foreach($cursor as $data) {
@@ -310,21 +316,17 @@ public function create($entityClass, array $data)
/**
* Find records with custom query
*
- * @throws \Spot\Exception
+ * @param string $entityName Name of the entity class
+ * @param string $sql Raw query or SQL to run against the datastore
+ * @param array Optional $conditions Array of binds in column => value pairs to use for prepared statement
*/
- public function query()
- {
- $args = func_get_args();
-
- // Remove entityName (first element)
- $entityName = array_shift($args);
-
- $result = $this->connection($entityName)->query($args);
+ public function query($entityName, $sql, array $params = array())
+ {
+ $result = $this->connection($entityName)->query($sql, $params);
if($result) {
- return $this->collection($result);
- } else {
- return false;
+ return $this->collection($entityName, $result);
}
+ return false;
}
@@ -599,18 +601,14 @@ public function validate($entity)
foreach($this->fields($entityName) as $field => $fieldAttrs) {
if(isset($fieldAttrs['required']) && true === $fieldAttrs['required']) {
// Required field
- if(empty($entity->$field)) {
- $this->error($field, "Required field '" . $field . "' was left blank");
+ if($this->isEmpty($entity->$field)) {
+ $entity->error($field, "Required field '" . $field . "' was left blank");
}
}
}
- // Check for errors
- if($this->hasErrors()) {
- return false;
- } else {
- return true;
- }
+ // Return error result
+ return !$entity->hasErrors();
}
@@ -622,18 +620,21 @@ public function validate($entity)
*/
public function isEmpty($value)
{
- return (empty($value) && 0 !== $value);
+ return empty($value) && !is_numeric($value);
}
/**
* Check if any errors exist
*
+ * @deprecated Please use Entity::hasErrors instead
* @param string $field OPTIONAL field name
* @return boolean
*/
public function hasErrors($field = null)
{
+ trigger_error('Error checks at the Mapper level have been deprecated in favor of checking error at the Entity level. Please use Entity::hasErrors instead. Mapper error methods will be completely removed in v1.0.', E_DEPRECATED);
+
if(null !== $field) {
return isset($this->_errors[$field]) ? count($this->_errors[$field]) : false;
}
@@ -644,13 +645,16 @@ public function hasErrors($field = null)
/**
* Get array of error messages
*
+ * @deprecated Please use Entity::errors instead
* @return array
*/
public function errors($msgs = null)
{
+ trigger_error('Error checks at the Mapper level have been deprecated in favor of checking error at the Entity level. Please use Entity::errors instead. Mapper error methods will be completely removed in v1.0.', E_DEPRECATED);
+
// Return errors for given field
if(is_string($msgs)) {
- return isset($this->_errors[$field]) ? $this->_errors[$field] : array();
+ return isset($this->_errors[$msgs]) ? $this->_errors[$msgs] : array();
// Set error messages from given array
} elseif(is_array($msgs)) {
@@ -670,6 +674,9 @@ public function errors($msgs = null)
*/
public function error($field, $msg)
{
+ // Deprecation warning
+ trigger_error('Adding errors at the Mapper level have been deprecated in favor of adding errors at the Entity level. Please use Entity::error instead. Mapper error methods will be completely removed in v1.0.', E_DEPRECATED);
+
if(is_array($msg)) {
// Add array of error messages about field
foreach($msg as $msgx) {
diff --git a/alloy/Plugin/Spot/lib/Spot/Query.php b/alloy/Plugin/Spot/lib/Spot/Query.php
index 4471de5..65d9ea4 100644
--- a/alloy/Plugin/Spot/lib/Spot/Query.php
+++ b/alloy/Plugin/Spot/lib/Spot/Query.php
@@ -280,9 +280,11 @@ public function params()
*/
public function count()
{
- // Execute query and return count
- $result = $this->execute();
- return ($result !== false) ? count($result) : 0;
+ // Execute query
+ $result = $this->mapper()->connection($this->entityName())->count($this);
+
+ //return count
+ return is_numeric($result) ? $result : 0;
}
diff --git a/alloy/Plugin/Spot/lib/Spot/Relation/RelationAbstract.php b/alloy/Plugin/Spot/lib/Spot/Relation/RelationAbstract.php
index a610ad9..a559053 100644
--- a/alloy/Plugin/Spot/lib/Spot/Relation/RelationAbstract.php
+++ b/alloy/Plugin/Spot/lib/Spot/Relation/RelationAbstract.php
@@ -38,7 +38,7 @@ public function __construct(\Spot\Mapper $mapper, \Spot\Entity $entity, array $r
// Checks ...
if(null === $this->_entityName) {
- throw new \InvalidArgumentException("Relation description key 'entity' not set.");
+ throw new \InvalidArgumentException("Relation description key 'entity' must be set to an Entity class name.");
}
}
@@ -119,8 +119,8 @@ public function relationOrder()
public function __toString()
{
// Load related records for current row
- $success = $this->findAllRelation();
- return ($success) ? "1" : "0";
+ $res = $this->execute();
+ return ($res) ? "1" : "0";
}
diff --git a/alloy/Plugin/Spot/lib/Spot/tests/Test/CRUD.php b/alloy/Plugin/Spot/lib/Spot/tests/Test/CRUD.php
index 0d9814d..029467e 100644
--- a/alloy/Plugin/Spot/lib/Spot/tests/Test/CRUD.php
+++ b/alloy/Plugin/Spot/lib/Spot/tests/Test/CRUD.php
@@ -77,6 +77,6 @@ public function testSampleNewsDelete()
$post = $mapper->first('Entity_Post', array('title' => "Test Post Modified"));
$result = $mapper->delete($post);
- $this->assertTrue($result);
+ $this->assertTrue((boolean) $result);
}
}
\ No newline at end of file
diff --git a/alloy/Plugin/Spot/lib/Spot/tests/Test/Config.php b/alloy/Plugin/Spot/lib/Spot/tests/Test/Config.php
index 204e030..7181f83 100644
--- a/alloy/Plugin/Spot/lib/Spot/tests/Test/Config.php
+++ b/alloy/Plugin/Spot/lib/Spot/tests/Test/Config.php
@@ -11,6 +11,22 @@ public function testAddConnectionWithDSNString()
{
$cfg = new \Spot\Config();
$adapter = $cfg->addConnection('test_mysql', 'mysql://test:password@localhost/test');
- $this->assertTrue($adapter instanceof \Spot\Adapter\Mysql);
+ $this->assertInstanceOf('\Spot\Adapter\Mysql', $adapter);
}
+
+ public function testConfigCanSerialize()
+ {
+ $cfg = new \Spot\Config();
+ $adapter = $cfg->addConnection('test_mysql', 'mysql://test:password@localhost/test');
+
+ $this->assertInternalType('string', serialize($cfg));
+ }
+
+ public function testConfigCanUnserialize()
+ {
+ $cfg = new \Spot\Config();
+ $adapter = $cfg->addConnection('test_mysql', 'mysql://test:password@localhost/test');
+
+ $this->assertInstanceOf('\Spot\Config', unserialize(serialize($cfg)));
+ }
}
\ No newline at end of file
diff --git a/alloy/Plugin/Spot/lib/Spot/tests/Test/Entity.php b/alloy/Plugin/Spot/lib/Spot/tests/Test/Entity.php
index 949ce95..0bedb91 100644
--- a/alloy/Plugin/Spot/lib/Spot/tests/Test/Entity.php
+++ b/alloy/Plugin/Spot/lib/Spot/tests/Test/Entity.php
@@ -54,4 +54,30 @@ public function testEntitySetDataConstruct()
$this->assertEquals($testData, $data);
}
+
+ public function testEntityErrors()
+ {
+ $post = new Entity_Post(array(
+ 'title' => 'My Awesome Post',
+ 'body' => 'Body
' + )); + $postErrors = array( + 'title' => array('Title cannot contain the word awesome') + ); + + // Has NO errors + $this->assertTrue(!$post->hasErrors()); + + // Set errors + $post->errors($postErrors); + + // Has errors + $this->assertTrue($post->hasErrors()); + + // Full error array + $this->assertEquals($postErrors, $post->errors()); + + // Errors for one key only + $this->assertEquals($postErrors['title'], $post->errors('title')); + } } \ No newline at end of file diff --git a/alloy/Plugin/Spot/lib/Spot/tests/Test/Relations.php b/alloy/Plugin/Spot/lib/Spot/tests/Test/Relations.php index 95824c7..6f23c2d 100644 --- a/alloy/Plugin/Spot/lib/Spot/tests/Test/Relations.php +++ b/alloy/Plugin/Spot/lib/Spot/tests/Test/Relations.php @@ -54,12 +54,12 @@ public function testBlogCommentsRelationInsertByObject($postId) 'name' => 'Testy McTester', 'email' => 'test@test.com', 'body' => 'This is a test comment. Yay!', - 'date_created' => $mapper->connection('Entity_Post_Comment')->dateTime() + 'date_created' => new \DateTime() )); try { $commentSaved = $mapper->save($comment); if(!$commentSaved) { - print_r($mapper->errors()); + print_r($comment->errors()); $this->fail("Comment NOT saved"); } } catch(Exception $e) { diff --git a/alloy/config/routes.php b/alloy/config/routes.php index 27e4076..013e173 100644 --- a/alloy/config/routes.php +++ b/alloy/config/routes.php @@ -16,7 +16,8 @@ ->post(array('action' => 'post')); $router->route('module', '/<:module>(.<:format>)') // :format optional - ->defaults(array('action' => 'index', 'format' => 'html')); + ->defaults(array('action' => 'index', 'format' => 'html')) + ->post(array('action' => 'post')); $router->route('default', '/') - ->defaults(array('module' => 'Home', 'action' => 'index', 'format' => 'html')); + ->defaults(array('module' => 'Home', 'action' => 'index', 'format' => 'html')); \ No newline at end of file diff --git a/alloy/lib/Alloy/Client.php b/alloy/lib/Alloy/Client.php deleted file mode 100644 index 6b3c07d..0000000 --- a/alloy/lib/Alloy/Client.php +++ /dev/null @@ -1,141 +0,0 @@ - value parameters to pass - */ - public function get($url, array $params = array()) - { - return $this->_fetch($url, $params, 'GET'); - } - - - /** - * POST - * - * @param string $url URL to perform action on - * @param optional array $params Array of key => value parameters to pass - */ - public function post($url, array $params = array()) - { - return $this->_fetch($url, $params, 'POST'); - } - - - /** - * PUT - * - * @param string $url URL to perform action on - * @param optional array $params Array of key => value parameters to pass - */ - public function put($url, array $params = array()) - { - return $this->_fetch($url, $params, 'PUT'); - } - - - /** - * DELETE - * - * @param string $url URL to perform action on - * @param optional array $params Array of key => value parameters to pass - */ - public function delete($url, array $params = array()) - { - return $this->_fetch($url, $params, 'DELETE'); - } - - - /** - * Fetch a URL with given parameters - */ - protected function _fetch($url, array $params = array(), $method = 'GET') - { - $method = strtoupper($method); - - $urlParts = parse_url($url); - $queryString = http_build_query($params); - - // Append params to URL as query string if not a POST - if(strtoupper($method) != 'POST') { - $url = $url . "?" . $queryString; - } - - //echo $url; - //var_dump("Fetching External URL: [" . $method . "] " . $url, $params); - - // Use cURL - if(function_exists('curl_init')) { - $ch = curl_init($urlParts['host']); - - // METHOD differences - switch($method) { - case 'GET': - curl_setopt($ch, CURLOPT_URL, $url . "?" . $queryString); - break; - case 'POST': - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $queryString); - break; - - case 'PUT': - curl_setopt($ch, CURLOPT_URL, $url); - $putData = file_put_contents("php://memory", $queryString); - curl_setopt($ch, CURLOPT_PUT, true); - curl_setopt($ch, CURLOPT_INFILE, $putData); - curl_setopt($ch, CURLOPT_INFILESIZE, strlen($queryString)); - break; - - case 'DELETE': - curl_setopt($ch, CURLOPT_URL, $url . "?" . $queryString); - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - break; - } - - - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return the data - curl_setopt($ch, CURLOPT_HEADER, false); // Get headers - - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_MAXREDIRS, 5); - - // HTTP digest authentication - if(isset($urlParts['user']) && isset($urlParts['pass'])) { - $authHeaders = array("Authorization: Basic ".base64_encode($urlParts['user'].':'.$urlParts['pass'])); - curl_setopt($ch, CURLOPT_HTTPHEADER, $authHeaders); - } - - $response = curl_exec($ch); - $responseInfo = curl_getinfo($ch); - curl_close($ch); - - // Use sockets... (eventually) - } else { - throw new Exception(__METHOD__ . " Requres the cURL library to work."); - } - - // Only return false on 404 or 500 errors for now - if($responseInfo['http_code'] == 404 || $responseInfo['http_code'] == 500) { - $response = false; - } - - return $response; - } -} \ No newline at end of file diff --git a/alloy/lib/Alloy/Kernel.php b/alloy/lib/Alloy/Kernel.php index 3dc07bc..add7f10 100644 --- a/alloy/lib/Alloy/Kernel.php +++ b/alloy/lib/Alloy/Kernel.php @@ -15,6 +15,8 @@ */ class Kernel { + const VERSION = '0.8.0'; + protected static $self; protected static $cfg = array(); @@ -63,10 +65,22 @@ protected function __construct(array $config = array()) // Save memory starting point static::$traceMemoryStart = memory_get_usage(); static::$traceTimeStart = microtime(true); + // Set last as current starting for good zero-base static::$traceMemoryLast = static::$traceMemoryStart; static::$traceTimeLast = static::$traceTimeStart; } + + + /** + * Return current Alloy version string + * + * @return string Current Alloy version in dot-notation + */ + public function version() + { + return static::VERSION; + } /** @@ -127,7 +141,7 @@ public function config($value = null, $default = false) */ public function factory($className, array $params = array()) { - $instanceHash = md5($className . var_export($params, true)); + $instanceHash = md5($className . serialize($params)); // Return already instantiated object instance if set if(isset($this->instances[$instanceHash])) { @@ -214,15 +228,6 @@ public function request() } - /** - * Get HTTP REST client - */ - public function client() - { - return $this->factory(__NAMESPACE__ . '\Client'); - } - - /** * Send HTTP response header * @@ -397,7 +402,7 @@ public function module($module, $init = true, $dispatchAction = null) * @throws \InvalidArgumentException When plugin is not found by name * @return object */ - public function plugin($plugin, $init = true) + public function plugin($plugin, array $pluginConfig = array(), $init = true) { // Module plugin // ex: 'Module\User' @@ -413,17 +418,42 @@ public function plugin($plugin, $init = true) } // Ensure class exists / can be loaded - if(!class_exists($sPluginClass, (boolean)$init)) { - if ($init) { - throw new \InvalidArgumentException("Unable to load plugin '" . $sPluginClass . "'. Remove from app config or ensure plugin files exist in 'app' or 'alloy' load paths."); + if(!class_exists($sPluginClass, (boolean) $init)) { + if($init) { + throw new \InvalidArgumentException("Unable to load plugin '" . $sPluginClass . "'. Remove from app configuration file or ensure plugin files exist in 'app' or 'alloy' load paths."); } return false; } // Instantiate module class - $sPluginObject = new $sPluginClass($this); - return $sPluginObject; + //$sPluginObject = new $sPluginClass($this); + return $this->factory($sPluginClass, array($this, $pluginConfig)); + } + + + /** + * Load plugins if provided + * + * @param array $plugins Array of plugin names to load or key => config array format + * Example: + * array('plugin1', 'plugin2', 'plugin3' => array('foo' => 'bar', 'bar' => 'baz'), 'plugin4') + */ + public function loadPlugins(array $plugins) + { + // Load plugins + if($plugins) { + foreach($plugins as $pluginName => $pluginConfig) { + // Config name supplied without config array - need to shift params + if(is_numeric($pluginName)) { + $pluginName = $pluginConfig; + $pluginConfig = array(); + } + + // Load plugin + $plugin = $this->plugin($pluginName, $pluginConfig); + } + } } @@ -800,4 +830,13 @@ public function __call($method, $args) throw new \BadMethodCallException("Method '" . __CLASS__ . "::" . $method . "' not found or the command is not a valid callback type."); } } + + + /** + * Prevent PHP from trying to serialize cached object instances on Kernel + */ + public function __sleep() + { + return array(); + } } \ No newline at end of file diff --git a/alloy/lib/Alloy/PluginAbstract.php b/alloy/lib/Alloy/PluginAbstract.php new file mode 100644 index 0000000..810d089 --- /dev/null +++ b/alloy/lib/Alloy/PluginAbstract.php @@ -0,0 +1,37 @@ +kernel = $kernel; + $this->config = $config; + + // Initialize plugin code + $this->init(); + } + + + /** + * Initialization work for plugin + */ + abstract public function init(); +} diff --git a/alloy/lib/Alloy/Request.php b/alloy/lib/Alloy/Request.php index 15ba86b..afcce9f 100644 --- a/alloy/lib/Alloy/Request.php +++ b/alloy/lib/Alloy/Request.php @@ -13,9 +13,12 @@ */ class Request { + // Request URL + protected $_url; + // Request parameters protected $_params = array(); - + /** * Ensure magic quotes are not mucking up request data @@ -32,16 +35,61 @@ public function __construct() array_walk_recursive($_COOKIE, $stripslashes_gpc); array_walk_recursive($_REQUEST, $stripslashes_gpc); } + + // Properly handle PUT and DELETE request params + if($this->isPut() || $this->isDelete()) { + parse_str(file_get_contents('php://input'), $params); + $this->params($params); + } } + + + /** + * Return requested URL path + * + * Works for HTTP(S) requests and CLI requests using the -u flag for URL dispatch emulation + * + * @return string Requested URL path segement + */ + public function url() + { + if(null === $this->_url) { + if($this->isCli()) { + // CLI request + $cliArgs = getopt("u:"); + + $requestUrl = isset($cliArgs['u']) ? $cliArgs['u'] : '/'; + $qs = parse_url($requestUrl, PHP_URL_QUERY); + $cliRequestParams = array(); + parse_str($qs, $cliRequestParams); + + // Set parsed query params back on request object + $this->setParams($cliRequestParams); + + // Set requestUrl and remove query string if present so router can parse it as expected + if($qsPos = strpos($requestUrl, '?')) { + $requestUrl = substr($requestUrl, 0, $qsPos); + } + + } else { + // HTTP request + $requestUrl = $this->get('u', '/'); + } + $this->_url = $requestUrl; + } + + return $this->_url; + } + /** - * Access values contained in the superglobals as public members - * Order of precedence: 1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV - * - * @see http://msdn.microsoft.com/en-us/library/system.web.httprequest.item.aspx - * @param string $key - * @return mixed - */ + * Access values contained in the superglobals as public members + * Order of precedence: 1. GET, 2. POST, 3. COOKIE, 4. SERVER, 5. ENV + * + * @see http://msdn.microsoft.com/en-us/library/system.web.httprequest.item.aspx + * @param string $key + * @return mixed + */ public function get($key, $default = null) { switch (true) { @@ -219,7 +267,8 @@ public function param($key = null, $default = null) public function query($key = null, $default = null) { if (null === $key) { - return $_GET; + // Return _GET params without routing param or other params set by Alloy or manually on the request object + return array_diff_key($_GET, $this->param() + array('u' => 1)); } return (isset($_GET[$key])) ? $_GET[$key] : $default; @@ -333,20 +382,64 @@ public function header($header) /** - * Return the method by which the request was made + * Return the method by which the request was made. Always returns HTTP_METHOD in UPPERCASE. * - * @return string + * @return string HTTP Request method in UPPERCASE */ public function method() { - $method = isset($_SERVER['REQUEST_METHOD']) ? strtoupper($_SERVER['REQUEST_METHOD']) : 'GET'; + $sm = strtoupper($this->server('REQUEST_METHOD', 'GET')); - // Emulate REST for browsers - if($method == "POST" && $this->post('_method')) { - $method = strtoupper($this->post('_method')); + // POST + '_method' override to emulate REST behavior in browsers that do not support it + if('POST' == $sm && $this->get('_method')) { + return strtoupper($this->get('_method')); } - return $method; + return $sm; + } + + + /** + * Request URI, as seen by PHP + * + * @return string request URI from $_SERVER superglobal + */ + public function uri() + { + return $this->server('REQUEST_URI'); + } + + + /** + * Request scheme (http, https, or cli) + * + * @return string 'http', 'https', or 'cli' + */ + public function scheme() + { + return ($this->isCli() ? 'cli' : ($this->isSecure() ? 'https' : 'http' )); + } + + + /** + * Request HTTP_HOST + * + * @return String request host from $_SERVER superglobal + */ + public function host() + { + return $this->server('HTTP_HOST'); + } + + + /** + * Request port + * + * @return integer request port + */ + public function port() + { + return $this->server('SERVER_PORT'); } @@ -435,6 +528,17 @@ public function isHead() { return ($this->method() == "HEAD"); } + + + /** + * Determine is incoming request is OPTIONS + * + * @return boolean + */ + public function isOptions() + { + return ($this->method() == "OPTIONS"); + } /** diff --git a/alloy/lib/Alloy/Response.php b/alloy/lib/Alloy/Response.php index 025b7e1..7249adb 100644 --- a/alloy/lib/Alloy/Response.php +++ b/alloy/lib/Alloy/Response.php @@ -93,6 +93,9 @@ public function encoding($encoding = null) */ public function content($content = null) { + if(null === $content) { + return (string) $this->_content; + } $this->_content = $content; } public function appendContent($content) diff --git a/alloy/lib/Alloy/Router.php b/alloy/lib/Alloy/Router.php index 0bfae8c..76efb6b 100644 --- a/alloy/lib/Alloy/Router.php +++ b/alloy/lib/Alloy/Router.php @@ -145,14 +145,17 @@ protected function routeMatch(Router_Route $route, $method, $url) throw new \InvalidArgumentException("Error matching URL to route params: matched(" . count($matches) . ") != named(" . count($namedParamsMatched) . ")"); } $params = array_combine(array_keys($namedParamsMatched), $matches); - - + if(strtoupper($method) != "GET") { - // Default REST behavior is to be 'greedy' and always use the REST method defaults if supplied - $params = array_merge($route->namedParams(), $route->defaults(), $params, $route->methodDefaults($method)); + // 1) Determine which actions are set in $params that are also in 'methodDefaults' + // 2) Override the 'methodDefaults' with the explicitly set $params + $setParams = array_filter(array_intersect_key($params, $route->methodDefaults($method))); + $methodParams = array_merge($route->namedParams(), $route->defaults(), $params, $route->methodDefaults($method), $setParams); + $params = $methodParams; } else { $params = array_merge($route->namedParams(), $route->defaults(), $route->methodDefaults($method), $params); } + //$params = array_merge($route->namedParams(), $route->defaults(), $route->methodDefaults($method), $params); } } return array_map('urldecode', $params); diff --git a/alloy/lib/Alloy/View/Generic/Treeview.php b/alloy/lib/Alloy/View/Generic/Treeview.php index 50932bf..ad092d8 100644 --- a/alloy/lib/Alloy/View/Generic/Treeview.php +++ b/alloy/lib/Alloy/View/Generic/Treeview.php @@ -198,6 +198,17 @@ public function filter($callback) } + /** + * Get currnet level + * + * @param int $level Current level + */ + public function level() + { + return self::$_level; + } + + /** * Set minimum level at which to begin item display * @@ -220,17 +231,6 @@ public function levelMax($level = null) $this->set('levelMax', $level); return $this; } - - - /** - * Get currnet level - * - * @param int $level Current level - */ - public function level() - { - return self::$_level; - } /** diff --git a/alloy/lib/Alloy/View/Generic/templates/cellgrid.html.php b/alloy/lib/Alloy/View/Generic/templates/cellgrid.html.php index a306e5c..875ae43 100644 --- a/alloy/lib/Alloy/View/Generic/templates/cellgrid.html.php +++ b/alloy/lib/Alloy/View/Generic/templates/cellgrid.html.php @@ -23,11 +23,11 @@ $ri = 0; endif; endforeach; - else: + elseif(isset($noDataCallback)): ?>