Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit a89f02d1d9e17abcf912adfa2e4713cca38a908e @tombenner committed Apr 3, 2011
Showing with 3,313 additions and 0 deletions.
  1. +2 −0 .gitignore
  2. +19 −0 LICENSE.txt
  3. +20 −0 README.md
  4. +120 −0 core/controllers/mvc_admin_controller.php
  5. +339 −0 core/controllers/mvc_controller.php
  6. +116 −0 core/controllers/mvc_public_controller.php
  7. +61 −0 core/functions/functions.php
  8. +288 −0 core/helpers/mvc_form_helper.php
  9. +184 −0 core/helpers/mvc_helper.php
  10. +58 −0 core/helpers/mvc_html_helper.php
  11. +46 −0 core/inflector.php
  12. +48 −0 core/models/mvc_database.php
  13. +193 −0 core/models/mvc_database_adapter.php
  14. +521 −0 core/models/mvc_model.php
  15. +59 −0 core/mvc_configuration.php
  16. +67 −0 core/mvc_dispatcher.php
  17. +58 −0 core/mvc_error.php
  18. +338 −0 core/mvc_loader.php
  19. +43 −0 core/mvc_model_registry.php
  20. +37 −0 core/mvc_object_registry.php
  21. +7 −0 core/pluggable/controllers/admin_controller.php
  22. +7 −0 core/pluggable/controllers/public_controller.php
  23. +7 −0 core/pluggable/helpers/form_helper.php
  24. +7 −0 core/pluggable/helpers/html_helper.php
  25. +7 −0 core/pluggable/models/app_model.php
  26. +52 −0 core/pluggable/views/admin/index.php
  27. +6 −0 core/pluggable/views/admin/layouts/admin.php
  28. +5 −0 core/pluggable/views/layouts/public.php
  29. +39 −0 core/router.php
  30. +14 −0 examples/events_calendar/app/config/bootstrap.php
  31. +41 −0 examples/events_calendar/app/controllers/admin/admin_events_controller.php
  32. +17 −0 examples/events_calendar/app/controllers/admin/admin_speakers_controller.php
  33. +7 −0 examples/events_calendar/app/controllers/admin/admin_venues_controller.php
  34. +35 −0 examples/events_calendar/app/controllers/events_controller.php
  35. +7 −0 examples/events_calendar/app/controllers/speakers_controller.php
  36. +19 −0 examples/events_calendar/app/controllers/venues_controller.php
  37. +54 −0 examples/events_calendar/app/models/event.php
  38. +33 −0 examples/events_calendar/app/models/speaker.php
  39. +18 −0 examples/events_calendar/app/models/venue.php
  40. +3 −0 examples/events_calendar/app/public/css/admin.css
  41. +9 −0 examples/events_calendar/app/views/admin/events/add.php
  42. +14 −0 examples/events_calendar/app/views/admin/events/edit.php
  43. +8 −0 examples/events_calendar/app/views/admin/speakers/add.php
  44. +8 −0 examples/events_calendar/app/views/admin/speakers/edit.php
  45. +9 −0 examples/events_calendar/app/views/admin/speakers/example_page.php
  46. +12 −0 examples/events_calendar/app/views/admin/venues/add.php
  47. +12 −0 examples/events_calendar/app/views/admin/venues/edit.php
  48. +9 −0 examples/events_calendar/app/views/events/_item.php
  49. +9 −0 examples/events_calendar/app/views/events/index.php
  50. +9 −0 examples/events_calendar/app/views/events/show.php
  51. +14 −0 examples/events_calendar/app/views/speakers/_info.php
  52. +3 −0 examples/events_calendar/app/views/speakers/_item.php
  53. +9 −0 examples/events_calendar/app/views/speakers/index.php
  54. +7 −0 examples/events_calendar/app/views/speakers/show.php
  55. +3 −0 examples/events_calendar/app/views/venues/_item.php
  56. +10 −0 examples/events_calendar/app/views/venues/index.php
  57. +17 −0 examples/events_calendar/app/views/venues/show.php
  58. +114 −0 examples/events_calendar/create_tables_and_insert_data.sql
  59. +35 −0 wp_mvc.php
2 .gitignore
@@ -0,0 +1,2 @@
+.DS_Store
+/app
19 LICENSE.txt
@@ -0,0 +1,19 @@
+Copyright (C) 2011 by Tom Benner
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
20 README.md
@@ -0,0 +1,20 @@
+WP MVC
+==================================================
+An MVC framework for WordPress
+
+Overview
+--------
+
+WP MVC is a WordPress plugin that allows developers to use the methodologies of an MVC framework inside of WordPress. Since WordPress already provides a large amount of functionality for the data types that it supports out of the box (users, posts, pages, comments, categories, tags, and links), the primary focus of WP MVC is on other data types. WordPress supports custom post types natively, of course, but setting up custom post types and all of the necessary related functionality (public views, administrative management, associations, etc) is typically more time-consuming than doing the equivalent work in an MVC framework. The resulting code and database structure is significantly less graceful than the MVC equivalent, too.
+
+WP MVC fills this gap. The basic idea is that you create an app/ directory that contains a file structure similar to other MVC frameworks (controllers/, helpers/, models/, views/, etc) and set up models, views, and controllers just as you would in other frameworks. WP MVC runs this code in the context of WordPress (i.e. you can still use all of WordPress's functionality inside of app/). Since WordPress already provides an administrative system, admin actions and views in app/ are run in that context, with WP MVC adding all of the necessary WordPress actions and filters to make this possible without the developer needing to lift a finger. An [Administration Menu](http://codex.wordpress.org/Administration_Menus) is automatically created for each model, but it can be customized or omitted.
+
+Getting Started
+---------------
+
+If you've worked with MVC frameworks, most of WP MVC should already be familiar. The quickest introduction is probably the example application that's located in examples/. To set it up, simply copy the app/ directory into the root of this plugin's directory (so that it's at plugins/wp_mvc/app/) and run its SQL script to set up the tables and some example data. After doing so, there will be administrative menus for each model in WordPress, and you'll be able to browse to URLs like /events/, /events/1/, /venues/, etc to see the public-facing views.
+
+Other Notes
+-----------
+
+This framework is still in development. Most of the functionality that's available is used in the example application, so if there's functionality that you'd like to use that isn't implemented in there, it may not exist yet. However, if it's something that is widely useful, I'd certainly be willing to implement it myself or to accept any well-written code that implements it. Please feel free to contact me through GitHub for any such requests.
120 core/controllers/mvc_admin_controller.php
@@ -0,0 +1,120 @@
+<?php
+
+class MvcAdminController extends MvcController {
+
+ public $is_admin = true;
+
+ public function index() {
+
+ $this->set_objects();
+
+ }
+
+ public function add() {
+
+ $this->create_or_save();
+
+ }
+
+ public function edit() {
+
+ $this->verify_id_param();
+ $this->set_object();
+ $this->create_or_save();
+
+ }
+
+ public function delete() {
+
+ $this->verify_id_param();
+ $this->set_object();
+ if (!empty($this->object)) {
+ $this->model->delete($this->params['id']);
+ $this->flash('notice', 'Successfully deleted!');
+ } else {
+ $this->flash('warning', 'A '.Inflector::humanize($this->model->name).' with ID "'.$this->params['id'].'" couldn\'t be found.');
+ }
+ $url = Router::admin_url(array('controller' => $this->name, 'action' => 'index'));
+ $this->redirect($url);
+
+ }
+
+ public function verify_id_param() {
+
+ if (empty($this->params['id'])) {
+ die('No ID specified');
+ }
+
+ }
+
+ public function create_or_save() {
+
+ if (!empty($this->params['data'])) {
+ if (!empty($this->params['data'][$this->model->name])) {
+ $object = $this->params['data'][$this->model->name];
+ if (empty($object['id'])) {
+ $model = $this->model;
+ $model->create($this->params['data']);
+ $id = $model->insert_id;
+ $url = Router::admin_url(array('controller' => $this->name, 'action' => 'edit', 'id' => $id));
+ $this->flash('notice', 'Successfully created!');
+ $this->redirect($url);
+ } else {
+ $this->model->save($this->params['data']);
+ $this->flash('notice', 'Successfully saved!');
+ $this->refresh();
+ }
+ }
+ }
+
+ }
+
+ public function set_objects() {
+
+ $this->params['page'] = empty($this->params['page_num']) ? 1 : $this->params['page_num'];
+
+ if (!empty($this->params['q'])) {
+ if (!empty($this->model->admin_searchable_fields)) {
+ $conditions = array();
+ foreach($this->model->admin_searchable_fields as $field) {
+ $conditions[] = array($field.' LIKE' => '%'.$this->params['q'].'%');
+ }
+ $this->params['conditions'] = array(
+ 'OR' => $conditions
+ );
+ }
+ if (!empty($this->model->admin_search_joins)) {
+ $this->params['joins'] = $this->model->admin_search_joins;
+ }
+ }
+
+ $collection = $this->model->paginate($this->params);
+
+ $this->set('objects', $collection['objects']);
+ $this->set_pagination($collection);
+
+ }
+
+ public function set_pagination($collection) {
+
+ $url_params = Router::admin_url_params(array('controller' => $this->name));
+ $params = $this->params;
+ unset($params['page_num']);
+ $params['page'] = $url_params['page'];
+ $this->set('pagination', array(
+ 'base' => get_admin_url().'admin.php%_%',
+ 'format' => '?page_num=%#%',
+ 'total' => $collection['total_pages'],
+ 'current' => $collection['page'],
+ 'add_args' => $params
+ ));
+
+ }
+
+ public function after_action($action) {
+
+ }
+
+}
+
+?>
339 core/controllers/mvc_controller.php
@@ -0,0 +1,339 @@
+<?php
+
+class MvcController {
+
+ public $name = '';
+ public $view_rendered = false;
+ public $view_vars = array();
+ public $is_controller = true;
+
+ function __construct() {
+
+ $this->set_meta();
+
+ }
+
+ public function init() {
+
+ $this->load_helper('Html');
+
+ $models = MvcModelRegistry::get_models();
+ foreach($models as $model_name => $model) {
+ $underscore = Inflector::underscore($model_name);
+ $tableize = Inflector::tableize($model_name);
+ // Add dynamicly created methods to HtmlHelper in the form speaker_url($object), speaker_link($object)
+ $method = $underscore.'_url';
+ $this->html->{$method} = create_function('$object, $options=array()', '
+ $defaults = array("controller" => "'.$tableize.'", "action" => "show");
+ $options = array_merge($defaults, $options);
+ return HtmlHelper::object_url($object, $options);
+ ');
+ $method = $underscore.'_link';
+ $this->html->{$method} = create_function('$object, $options=array()', '
+ $defaults = array("controller" => "'.$tableize.'", "action" => "show");
+ $options = array_merge($defaults, $options);
+ return HtmlHelper::object_link($object, $options);
+ ');
+ }
+
+ if (is_admin()) {
+ $this->load_helper('Form');
+ }
+
+ $model = $this->model->name;
+
+ if (class_exists($model.'Helper')) {
+ $helper_name = $model.'Helper';
+ } else if (class_exists('AppHelper')) {
+ $helper_name = 'AppHelper';
+ } else {
+ $helper_name = 'MvcHelper';
+ }
+ $this->helper = new $helper_name();
+
+ }
+
+ public function index() {
+
+ }
+
+ private function set_meta() {
+ $model = get_class($this);
+ $model = preg_replace('/Controller$/', '', $model);
+ $this->name = Inflector::underscore($model);
+ $this->views_path = '';
+ if (preg_match('/^Admin[A-Z]/', $model)) {
+ $this->views_path = 'admin/';
+ $model = preg_replace('/^Admin/', '', $model);
+ }
+
+ $model = Inflector::singularize($model);
+ $this->views_path .= Inflector::tableize($model).'/';
+ $this->model_name = $model;
+ // To do: remove the necessity of this redundancy
+ $this->model = new $model();
+ $this->{$model} = new $model();
+ }
+
+ protected function load_helper($helper_name) {
+ $helper_name = $helper_name.'Helper';
+ $helper_underscore = Inflector::underscore($helper_name);
+
+ $app_path = MVC_PLUGIN_PATH.'app/helpers/'.$helper_underscore.'.php';
+ if (file_exists($app_path)) {
+ require_once $app_path;
+ } else {
+ require_once MVC_PLUGIN_PATH.'core/pluggable/helpers/'.$helper_underscore.'.php';
+ }
+
+ if (class_exists($helper_name)) {
+ $helper_method_name = str_replace('_helper', '', $helper_underscore);
+
+ $this->{$helper_method_name} = new $helper_name();
+
+ if ($helper_name == 'FormHelper') {
+ $this->{$helper_method_name}->controller = false;
+ $this->{$helper_method_name}->controller->action = $this->action;
+ $this->{$helper_method_name}->controller->name = $this->name;
+ }
+ }
+ }
+
+ protected function load_model($model_name) {
+ $model_underscore = Inflector::underscore($model_name);
+
+ $app_path = MVC_PLUGIN_PATH.'app/models/'.$model_underscore.'.php';
+ if (file_exists($app_path)) {
+ require_once $app_path;
+ } else {
+ require_once MVC_PLUGIN_PATH.'core/pluggable/models/'.$model_underscore.'.php';
+ }
+
+ if (class_exists($model_name)) {
+ $this->{$model_name} = new $model_name();
+ }
+ }
+
+ protected function load_models($model_names) {
+ foreach($model_names as $model_name) {
+ $this->load_model($model_name);
+ }
+ }
+
+ public function set_object() {
+ if (!empty($this->params['id'])) {
+ $object = $this->model->find_by_id($this->params['id']);
+ if (!empty($object)) {
+ $this->set('object', $object);
+ MvcObjectRegistry::add_object($this->model->name, &$this->object);
+ return true;
+ }
+ }
+ MvcError::warning('Object not found.');
+ return false;
+ }
+
+ public function set($variable_name_or_array, $data=null) {
+ if (is_string($variable_name_or_array)) {
+ $this->set_view_var($variable_name_or_array, $data);
+ } else if (is_array($variable_name_or_array)) {
+ foreach($variable_name_or_array as $key => $value) {
+ $this->set_view_var($key, $value);
+ }
+ }
+ }
+
+ public function render_view($path, $options=array()) {
+
+ // Rendering from within a controller
+ if ($this->is_controller) {
+ if ($this->is_admin) {
+ $layout = empty($options['layout']) ? 'admin' : $options['layout'];
+ $layout_directory = 'admin/layouts/';
+ } else {
+ $layout = empty($options['layout']) ? 'public' : $options['layout'];
+ $layout_directory = 'layouts/';
+ }
+ $this->main_view = $path;
+ // We're now entering the view, so $this should no longer be a controller
+ $this->is_controller = false;
+ $this->render_view_with_view_vars($layout_directory.$layout, $options);
+ if (!$this->is_admin) {
+ die();
+ }
+ // Rendering from within a view
+ } else {
+ $this->render_view_from_view($path, $options);
+ }
+
+ }
+
+ public function render_main_view() {
+ $this->main_view;
+ $this->render_view_with_view_vars($this->main_view);
+ }
+
+ protected function render_view_with_view_vars($path, $options=array()) {
+
+ $view_vars = array(
+ 'model' => $this->model,
+ 'views_path' => $this->views_path,
+ 'helper' => $this->helper
+ );
+
+ if (!empty($options['locals'])) {
+ $view_vars = array_merge($view_vars, $options['locals']);
+ }
+
+ $this->view_vars = array_merge($this->view_vars, $view_vars);
+ $this->include_view($path, $this->view_vars);
+ $this->view_rendered = true;
+
+ }
+
+ protected function render_view_from_view($path, $options=array()) {
+
+ $view_vars = array();
+
+ if (strpos($path, '/') === false) {
+ $path = $this->name.'/'.$path;
+ }
+
+ if (!empty($options['collection'])) {
+ $var_name = empty($options['as']) ? 'object' : $options['as'];
+ foreach($options['collection'] as $object) {
+ $view_vars = array();
+ $view_vars[$var_name] = $object;
+ if (!empty($options['locals'])) {
+ $view_vars = array_merge($view_vars, $options['locals']);
+ }
+ $this->include_view($path, $view_vars);
+ }
+ return;
+ }
+
+ if (!empty($options['locals'])) {
+ $view_vars = $options['locals'];
+ }
+
+ $this->include_view($path, $view_vars);
+
+ }
+
+ protected function include_view($path, $view_vars=array()) {
+
+ extract($view_vars);
+
+ $include_path = MVC_PLUGIN_PATH.'app/views/'.$path.'.php';
+
+ if (file_exists($include_path)) {
+ require $include_path;
+ } else {
+ $path = preg_replace('/admin\/(?!layouts)([\w_]+)/', 'admin', $path);
+ $include_path = MVC_PLUGIN_PATH.'core/pluggable/views/'.$path.'.php';
+ if (file_exists($include_path)) {
+ require $include_path;
+ } else {
+ MvcError::warning('View "'.$path.'" not found.');
+ }
+ }
+
+ }
+
+ private function set_view_var($key, $value) {
+ if ($key == 'object') {
+ $this->object = $value;
+ }
+ $this->view_vars[$key] = $value;
+ }
+
+ protected function set_flash($type, $message) {
+ $this->init_flash();
+ $_SESSION['mvc_flash'][$type] = $message;
+ }
+
+ protected function unset_flash($type) {
+ $this->init_flash();
+ unset($_SESSION['mvc_flash'][$type]);
+ }
+
+ protected function get_flash($type) {
+ $this->init_flash();
+ $message = empty($_SESSION['mvc_flash'][$type]) ? null : $_SESSION['mvc_flash'][$type];
+ return $message;
+ }
+
+ protected function get_all_flashes() {
+ $this->init_flash();
+ return $_SESSION['mvc_flash'];
+ }
+
+ public function flash($type, $message=null) {
+ if (func_num_args() == 1) {
+ $message = $this->get_flash($type);
+ $this->unset_flash($type);
+ return $message;
+ }
+ $this->set_flash($type, $message);
+ }
+
+ public function display_flash() {
+ $flashes = $this->get_all_flashes();
+ $html = '';
+ if (!empty($flashes)) {
+ foreach($flashes as $type => $message) {
+ $classes = array();
+ $classes[] = $type;
+ if ($this->is_admin) {
+ if ($type == 'notice') {
+ $classes[] = 'updated';
+ }
+ }
+ $html .= '
+ <div id="message" class="'.implode(' ', $classes).'">
+ <p>
+ '.$message.'
+ </p>
+ </div>';
+ $this->unset_flash($type);
+ }
+ }
+ echo $html;
+ }
+
+ private function init_flash() {
+ if (!isset($_SESSION['mvc_flash'])) {
+ $_SESSION['mvc_flash'] = array();
+ }
+ }
+
+ public function refresh() {
+ $location = $this->current_url();
+ $this->redirect($location);
+ }
+
+ public function redirect($location, $status=302) {
+
+ // MvcDispatcher::dispatch() doesn't run until after the WP has already begun to print out HTML, unfortunately, so
+ // this will almost always be done with JS instead of wp_redirect().
+ if (headers_sent()) {
+ $html = '
+ <script type="text/javascript">
+ window.location = "'.$location.'";
+ </script>';
+ echo $html;
+ } else {
+ wp_redirect($location, $status);
+ }
+
+ die();
+
+ }
+
+ public function current_url() {
+ return $_SERVER['REQUEST_URI'];
+ }
+
+}
+
+?>
116 core/controllers/mvc_public_controller.php
@@ -0,0 +1,116 @@
+<?php
+
+class MvcPublicController extends MvcController {
+
+ public $is_admin = false;
+
+ function __construct() {
+
+ parent::__construct();
+
+ $this->clean_wp_query();
+
+ }
+
+ public function index() {
+
+ $this->set_objects();
+
+ }
+
+ public function show() {
+
+ $this->set_object();
+
+ }
+
+ public function set_objects() {
+
+ $this->params['page'] = empty($this->params['page']) ? 1 : $this->params['page'];
+
+ if (!empty($this->params['q'])) {
+ if (!empty($this->model->public_searchable_fields)) {
+ $conditions = array();
+ foreach($this->model->public_searchable_fields as $field) {
+ $conditions[] = array($field.' LIKE' => '%'.$this->params['q'].'%');
+ }
+ $this->params['conditions'] = array(
+ 'OR' => $conditions
+ );
+ }
+ }
+
+ $collection = $this->model->paginate($this->params);
+
+ $this->set('objects', $collection['objects']);
+ $this->set_pagination($collection);
+
+ }
+
+ public function set_pagination($collection) {
+ $params = $this->params;
+ unset($params['page']);
+ $url = Router::public_url(array('controller' => $this->name));
+ $this->pagination = array(
+ 'base' => $url.'%_%',
+ 'format' => '?page=%#%',
+ 'total' => $collection['total_pages'],
+ 'current' => $collection['page'],
+ 'add_args' => $params
+ );
+ }
+
+ public function pagination($options=array()) {
+ return paginate_links($this->pagination);
+ }
+
+ public function set_page_title() {
+ add_filter('wp_title', array($this, 'set_wp_title'));
+ }
+
+ public function after_action($action) {
+ $this->set_page_title();
+ }
+
+ public function set_wp_title($original_title) {
+ $separator = ' | ';
+ $controller_name = Inflector::titleize($this->name);
+ $object_name = null;
+ $object = null;
+ if ($this->action) {
+ if ($this->action == 'show' && is_object($this->object)) {
+ $object = $this->object;
+ if (!empty($this->object->__name)) {
+ $object_name = $this->object->__name;
+ }
+ }
+ }
+ $pieces = array(
+ $object_name,
+ $controller_name
+ );
+ $pieces = array_filter($pieces);
+ $title = implode($separator, $pieces);
+ $title = $title.$separator;
+ $title_options = apply_filters('mvc_page_title', array(
+ 'controller' => $controller_name,
+ 'action' => $this->action,
+ 'object_name' => $object_name,
+ 'object' => $object,
+ 'title' => $title
+ ));
+ $title = $title_options['title'];
+ return $title;
+ }
+
+ protected function clean_wp_query() {
+ global $wp_query;
+ $wp_query->is_single = false;
+ $wp_query->is_page = false;
+ $wp_query->queried_object = null;
+ $wp_query->is_home = false;
+ }
+
+}
+
+?>
61 core/functions/functions.php
@@ -0,0 +1,61 @@
+<?php
+
+function mvc_app_path() {
+ return MVC_APP_PATH;
+}
+
+function mvc_app_url() {
+
+ $abspath = rtrim(ABSPATH, '/').'/';
+ $site_url = rtrim(site_url(), '/').'/';
+
+ $url = str_replace($abspath, $site_url, mvc_app_path());
+ return $url;
+
+}
+
+function mvc_public_url($options) {
+ return Router::public_url($options);
+}
+
+function mvc_admin_url($options) {
+ return Router::admin_url($options);
+}
+
+function mvc_css_url($filename, $options=array()) {
+
+ $defaults = array(
+ 'add_extension' => true
+ );
+
+ $options = array_merge($defaults, $options);
+
+ if ($options['add_extension']) {
+ if (!preg_match('/\.[\w]{2,4}/', $filename)) {
+ $filename .= '.css';
+ }
+ }
+
+ return mvc_app_url().'public/css/'.$filename;
+
+}
+
+function mvc_js_url($filename, $options=array()) {
+
+ $defaults = array(
+ 'add_extension' => true
+ );
+
+ $options = array_merge($defaults, $options);
+
+ if ($options['add_extension']) {
+ if (!preg_match('/\.[\w]{2,4}/', $filename)) {
+ $filename .= '.js';
+ }
+ }
+
+ return mvc_app_url().'public/js/'.$filename;
+
+}
+
+?>
288 core/helpers/mvc_form_helper.php
@@ -0,0 +1,288 @@
+<?php
+
+class MvcFormHelper extends MvcHelper {
+
+ public function create($model_name, $options=array()) {
+ $defaults = array(
+ 'action' => $this->controller->action,
+ 'controller' => Inflector::tableize($model_name)
+ );
+ $options = array_merge($defaults, $options);
+ $this->model_name = $model_name;
+ $this->object = MvcObjectRegistry::get_object($model_name);
+ $this->model = MvcModelRegistry::get_model($model_name);
+ $this->schema = $this->model->schema;
+ $object_id = !empty($this->object) && !empty($this->object->__id) ? $this->object->__id : null;
+ $router_options = array('controller' => $options['controller'], 'action' => $options['action']);
+ if ($object_id) {
+ $router_options['id'] = $object_id;
+ }
+ $html = '<form action="'.Router::admin_url($router_options).'" method="post">';
+ if ($object_id) {
+ $html .= '<input type="hidden" name="'.$this->input_name('id').'" value="'.$object_id.'" />';
+ }
+ return $html;
+ }
+
+ public function end($label='Submit') {
+ $html = '<div><input type="submit" value="'.$this->esc_attr($label).'" /></div>';
+ $html .= '</form>';
+ return $html;
+ }
+
+ // Generalized method that chooses the appropriate input type based on the SQL type of the field
+ public function input($field_name, $options=array()) {
+ if (!empty($this->schema[$field_name])) {
+ $schema = $this->schema[$field_name];
+ $type = $this->get_type_from_sql_schema($schema);
+ $defaults = array(
+ 'type' => $type,
+ 'label' => Inflector::titleize($schema['field']),
+ 'value' => empty($this->object->$field_name) ? '' : $this->object->$field_name
+ );
+ if ($type == 'checkbox') {
+ unset($defaults['value']);
+ }
+ $options = array_merge($defaults, $options);
+ $options['type'] = empty($options['type']) ? 'text' : $options['type'];
+ $html = $this->{$options['type'].'_input'}($field_name, $options);
+ return $html;
+ } else {
+ MvcError::fatal('Field "'.$field_name.'" not found for use in a form input.');
+ return '';
+ }
+ }
+
+ public function text_input($field_name, $options=array()) {
+ $defaults = array(
+ 'id' => $this->input_id($field_name),
+ 'name' => $this->input_name($field_name),
+ 'type' => 'text'
+ );
+ $options = array_merge($defaults, $options);
+ $attributes_html = self::attributes_html($options, 'input');
+ $html = $this->before_input($field_name, $options);
+ $html .= '<input'.$attributes_html.' />';
+ $html .= $this->after_input($field_name, $options);
+ return $html;
+ }
+
+ public function textarea_input($field_name, $options=array()) {
+ $defaults = array(
+ 'id' => $this->input_id($field_name),
+ 'name' => $this->input_name($field_name),
+ 'type' => 'text'
+ );
+ $options = array_merge($defaults, $options);
+ $html = $this->before_input($field_name, $options);
+ $html .= '<textarea id="'.$options['id'].'" name="'.$this->input_name($field_name).'">'.$options['value'].'</textarea>';
+ $html .= $this->after_input($field_name, $options);
+ return $html;
+ }
+
+ public function checkbox_input($field_name, $options=array()) {
+ $defaults = array(
+ 'id' => $this->input_id($field_name),
+ 'name' => $this->input_name($field_name),
+ 'type' => 'checkbox',
+ 'checked' => !empty($this->object->$field_name) && $this->object->$field_name ? true : false,
+ 'value' => '1',
+ 'include_hidden_input' => true
+ );
+ $options = array_merge($defaults, $options);
+ if (!$options['checked']) {
+ unset($options['checked']);
+ } else {
+ $options['checked'] = 'checked';
+ }
+ $attributes_html = self::attributes_html($options, 'input');
+ $html = $this->before_input($field_name, $options);
+ if ($options['include_hidden_input']) {
+ // Included to allow for a workaround to the issue of unchecked checkbox fields not being sent by clients
+ $html .= '<input type="hidden" name="'.$this->esc_attr($options['name']).'" value="0" />';
+ }
+ $html .= '<input'.$attributes_html.' />';
+ $html .= $this->after_input($field_name, $options);
+ return $html;
+ }
+
+ public function select($field_name, $options=array()) {
+ $html = $this->before_input($field_name, $options);
+ $html .= $this->select_tag($field_name, $options);
+ $html .= $this->after_input($field_name, $options);
+ return $html;
+ }
+
+ public function select_tag($field_name, $options=array()) {
+ $defaults = array(
+ 'empty' => false,
+ 'value' => null
+ );
+
+ $options = array_merge($defaults, $options);
+ $options['options'] = empty($options['options']) ? array() : $options['options'];
+ $options['name'] = $field_name;
+ $attributes_html = self::attributes_html($options, 'select');
+ $html = '<select'.$attributes_html.'>';
+ if ($options['empty']) {
+ $empty_name = is_string($options['empty']) ? $options['empty'] : '';
+ $html .= '<option value="">'.$empty_name.'</option>';
+ }
+ foreach($options['options'] as $key => $value) {
+ if (is_object($value)) {
+ $key = $value->__id;
+ $value = $value->__name;
+ }
+ $selected_attribute = $options['value'] == $key ? ' selected="selected"' : '';
+ $html .= '<option value="'.$this->esc_attr($key).'"'.$selected_attribute.'>'.$value.'</option>';
+ }
+ $html .= '</select>';
+ return $html;
+ }
+
+ public function belongs_to_dropdown($model_name, $select_options, $options=array()) {
+
+ if (!empty($this->model->associations[$model_name])) {
+ $foreign_key = $this->model->associations[$model_name]['foreign_key'];
+ } else {
+ $foreign_key = Inflector::underscore($model_name).'_id';
+ }
+
+ $value = empty($this->object->{$foreign_key}) ? '' : $this->object->{$foreign_key};
+
+ $defaults = array(
+ 'id' => $this->model_name.'_'.$model_name.'_select',
+ 'name' => 'data['.$this->model_name.']['.$foreign_key.']',
+ 'label' => Inflector::titleize($model_name),
+ 'value' => $value,
+ 'options' => $select_options,
+ 'empty' => true
+ );
+ $options = array_merge($defaults, $options);
+ $select_options = $options;
+ $select_options['label'] = null;
+ $select_options['before'] = null;
+ $select_options['after'] = null;
+
+ $field_name = $options['name'];
+
+ $html = $this->before_input($field_name, $options);
+ $html .= $this->select_tag($field_name, $select_options);
+ $html .= $this->after_input($field_name, $options);
+
+ return $html;
+ }
+
+ public function has_many_dropdown($model_name, $select_options, $options=array()) {
+ $defaults = array(
+ 'select_id' => $this->model_name.'_'.$model_name.'_select',
+ 'select_name' => $this->model_name.'_'.$model_name.'_select',
+ 'list_id' => $this->model_name.'_'.$model_name.'_list',
+ 'ids_input_name' => 'data['.$this->model_name.']['.$model_name.'][ids]',
+ 'label' => Inflector::pluralize(Inflector::titleize($model_name)),
+ 'options' => $select_options
+ );
+ $options = array_merge($defaults, $options);
+
+ $select_options = $options;
+ $select_options['id'] = $select_options['select_id'];
+
+ $html = $this->before_input($options['select_name'], $select_options);
+ $html .= $this->select_tag($options['select_name'], $select_options);
+
+ $associated_objects = empty($this->object->{Inflector::tableize($model_name)}) ? array() : $this->object->{Inflector::tableize($model_name)};
+
+ // An empty value is necessary to ensure that data with name $options['ids_input_name'] is submitted; otherwise,
+ // if no association objects were selected the save() method wouldn't know that this association data is being
+ // updated and that it should, as a result, delete existing association data.
+ $html .= '<input type="hidden" name="'.$options['ids_input_name'].'[]" value="" />';
+
+ $html .= '<ul id="'.$options['list_id'].'">';
+ foreach($associated_objects as $associated_object) {
+ $html .= '
+ <li>
+ '.$associated_object->__name.'
+ <a href="#" class="remove-item">Remove</a>
+ <input type="hidden" name="'.$options['ids_input_name'].'[]" value="'.$associated_object->__id.'" />
+ </li>';
+ }
+ $html .= '</ul>';
+
+ $html .= '
+
+ <script type="text/javascript">
+
+ jQuery(document).ready(function(){
+
+ jQuery("#'.$options['select_id'].'").change(function() {
+ var option = jQuery(this).find("option:selected");
+ var id = option.attr("value");
+ if (id) {
+ var name = option.text();
+ var list_item = \'<li><input type="hidden" name="'.$options['ids_input_name'].'[]" value="\'+id+\'" />\'+name+\' <a href="#" class="remove-item">Remove</a></li>\';
+ jQuery("#'.$options['list_id'].'").append(list_item);
+ }
+ return false;
+ });
+
+ jQuery(".remove-item").live("click", function() {
+ jQuery(this).parents("li:first").remove();
+ return false;
+ });
+
+ });
+
+ </script>
+
+ ';
+ $html .= $this->after_input($options['select_name'], $select_options);
+
+ return $html;
+
+ }
+
+ private function before_input($field_name, $options) {
+ $defaults = array(
+ 'before' => '<div>'
+ );
+ $options = array_merge($defaults, $options);
+ $html = $options['before'];
+ if (!empty($options['label'])) {
+ $html .= '<label for="'.$options['id'].'">'.$options['label'].'</label>';
+ }
+ return $html;
+ }
+
+ private function after_input($field_name, $options) {
+ $defaults = array(
+ 'after' => '</div>'
+ );
+ $options = array_merge($defaults, $options);
+ $html = $options['after'];
+ return $html;
+ }
+
+ private function input_id($field_name) {
+ return $this->model_name.Inflector::camelize($field_name);
+ }
+
+ private function input_name($field_name) {
+ return 'data['.$this->model_name.']['.Inflector::underscore($field_name).']';
+ }
+
+ private function get_type_from_sql_schema($schema) {
+ switch($schema['type']) {
+ case 'varchar':
+ return 'text';
+ case 'text':
+ return 'textarea';
+
+ }
+ if ($schema['type'] == 'tinyint' && $schema['length'] == '1') {
+ return 'checkbox';
+ }
+ }
+
+}
+
+?>
184 core/helpers/mvc_helper.php
@@ -0,0 +1,184 @@
+<?php
+
+class MvcHelper {
+
+ public function render_view($path) {
+
+ require_once MVC_PLUGIN_PATH.'app/views/'.$path.'.php';
+
+ }
+
+ public function esc_attr($string) {
+ return esc_attr($string);
+ }
+
+ public function attributes_html($attributes, $valid_attributes_array_or_tag) {
+
+ $event_attributes = array(
+ 'standard' => array(
+ 'onclick',
+ 'ondblclick',
+ 'onkeydown',
+ 'onkeypress',
+ 'onkeyup',
+ 'onmousedown',
+ 'onmousemove',
+ 'onmouseout',
+ 'onmouseover',
+ 'onmouseup'
+ ),
+ 'form' => array(
+ 'onblur',
+ 'onchange',
+ 'onfocus',
+ 'onreset',
+ 'onselect',
+ 'onsubmit'
+ )
+ );
+
+ // To do: add on* event attributes
+ $valid_attributes_by_tag = array(
+ 'a' => array(
+ 'accesskey',
+ 'charset',
+ 'class',
+ 'dir',
+ 'coords',
+ 'href',
+ 'hreflang',
+ 'id',
+ 'lang',
+ 'name',
+ 'rel',
+ 'rev',
+ 'shape',
+ 'style',
+ 'tabindex',
+ 'target',
+ 'title',
+ 'xml:lang'
+ ),
+ 'input' => array(
+ 'accept',
+ 'access_key',
+ 'alt',
+ 'checked',
+ 'class',
+ 'dir',
+ 'disabled',
+ 'id',
+ 'lang',
+ 'maxlength',
+ 'name',
+ 'readonly',
+ 'size',
+ 'src',
+ 'style',
+ 'tabindex',
+ 'title',
+ 'type',
+ 'value',
+ 'xml:lang',
+ $event_attributes['form']
+ ),
+ 'select' => array(
+ 'class',
+ 'dir',
+ 'disabled',
+ 'id',
+ 'lang',
+ 'multiple',
+ 'name',
+ 'size',
+ 'style',
+ 'tabindex',
+ 'title',
+ 'xml:lang',
+ $event_attributes['form']
+ )
+ );
+
+ foreach($valid_attributes_by_tag as $key => $valid_attributes) {
+ $valid_attributes = array_merge($event_attributes['standard'], $valid_attributes);
+ $valid_attributes = self::array_flatten($valid_attributes);
+ $valid_attributes_by_tag[$key] = $valid_attributes;
+ }
+
+ $valid_attributes = is_array($valid_attributes_array_or_tag) ? $valid_attributes_array_or_tag : $valid_attributes_by_tag[$valid_attributes_array_or_tag];
+
+ $attributes = array_intersect_key($attributes, array_flip($valid_attributes));
+
+ $attributes_html = '';
+ foreach($attributes as $key => $value) {
+ $attributes_html .= ' '.$key.'="'.esc_attr($value).'"';
+ }
+ return $attributes_html;
+
+ }
+
+ // Move these into an AdminHelper
+
+ public function admin_header_cells($model) {
+ $html = '';
+ foreach($model->admin_columns as $key => $column) {
+ $html .= $this->admin_header_cell($column['label']);
+ }
+ $html .= $this->admin_header_cell('');
+ return '<tr>'.$html.'</tr>';
+
+ }
+
+ public function admin_header_cell($label) {
+ return '<th scope="col" class="manage-column">'.$label.'</th>';
+ }
+
+ public function admin_table_cells($model, $objects) {
+ $html = '';
+ foreach($objects as $object) {
+ $html .= '<tr>';
+ foreach($model->admin_columns as $key => $column) {
+ $html .= $this->admin_table_cell($model, $object, $column);
+ }
+ $html .= $this->admin_actions_cell($model, $object);
+ $html .= '</tr>';
+ }
+ return $html;
+ }
+
+ public function admin_table_cell($model, $object, $column) {
+ if (!empty($column['value_method'])) {
+ $value = $model->{$column['value_method']}($object);
+ } else {
+ $value = $object->$column['key'];
+ }
+ return '<td>'.$value.'</td>';
+ }
+
+ public function admin_actions_cell($model, $object) {
+ $links = array();
+ $object_name = empty($object->__name) ? 'Item #'.$object->__id : $object->__name;
+ $encoded_object_name = $this->esc_attr($object_name);
+ $controller = Inflector::tableize($model->name);
+ $links[] = '<a href="'.Router::admin_url(array('controller' => $controller, 'action' => 'edit', 'id' => $object->__id)).'" title="Edit '.$encoded_object_name.'">Edit</a>';
+ $links[] = '<a href="'.Router::public_url(array('controller' => $controller, 'action' => 'show', 'id' => $object->__id)).'" title="View '.$encoded_object_name.'">View</a>';
+ $links[] = '<a href="'.Router::admin_url(array('controller' => $controller, 'action' => 'delete', 'id' => $object->__id)).'" title="Delete '.$encoded_object_name.'" onclick="return confirm(&#039;Are you sure you want to delete '.$encoded_object_name.'?&#039;);">Delete</a>';
+ $html = implode(' | ', $links);
+ return '<td>'.$html.'</td>';
+ }
+
+ // To do: move this into an MvcUtilities class (?)
+
+ private function array_flatten($array) {
+
+ foreach($array as $key => $value){
+ $array[$key] = (array)$value;
+ }
+
+ return call_user_func_array('array_merge', $array);
+
+ }
+
+}
+
+?>
58 core/helpers/mvc_html_helper.php
@@ -0,0 +1,58 @@
+<?php
+
+class MvcHtmlHelper extends MvcHelper {
+
+ public function link($title, $url, $options=array()) {
+
+ if (is_array($url)) {
+ $url = Router::public_url($url);
+ }
+
+ $defaults = array(
+ 'href' => $url,
+ 'title' => $title
+ );
+ $options = array_merge($defaults, $options);
+
+ $attributes_html = self::attributes_html($options, 'a');
+
+ $html = '<a'.$attributes_html.'>'.$title.'</a>';
+ return $html;
+
+ }
+
+ public function object_url($object, $options) {
+ $options['id'] = $object->__id;
+ $url = Router::public_url($options);
+ return $url;
+ }
+
+ public function object_link($object, $options) {
+ $url = self::object_url($object, $options);
+ $title = empty($options['title']) ? $object->__name : $options['title'];
+ return self::link($title, $url);
+ }
+
+ public function admin_object_url($object, $options) {
+ $options['id'] = $object->__id;
+ $url = Router::admin_url($options);
+ return $url;
+ }
+
+ public function admin_object_link($object, $options) {
+ $url = self::admin_object_url($object, $options);
+ $title = $object->__name;
+ return self::link($title, $url);
+ }
+
+ public function __call($method, $args) {
+ if (property_exists($this, $method)) {
+ if (is_callable($this->$method)) {
+ return call_user_func_array($this->$method, $args);
+ }
+ }
+ }
+
+}
+
+?>
46 core/inflector.php
@@ -0,0 +1,46 @@
+<?php
+
+class Inflector {
+
+ public function class_name_from_filename($filename) {
+ return Inflector::camelize(str_replace('.php', '', $filename));
+ }
+
+ public function camelize($string) {
+ $string = str_replace('_', ' ', $string);
+ $string = ucwords($string);
+ $string = str_replace(' ', '', $string);
+ return $string;
+ }
+
+ public function tableize($string) {
+ $string = Inflector::underscore($string);
+ $string = Inflector::pluralize($string);
+ return $string;
+ }
+
+ public function underscore($string) {
+ $string = preg_replace('/[A-Z]/', ' $0', $string);
+ $string = trim(strtolower($string));
+ $string = str_replace(' ', '_', $string);
+ return $string;
+ }
+
+ public function pluralize($string) {
+ return $string.'s';
+ }
+
+ public function singularize($string) {
+ return preg_replace('/s$/', '', $string);
+ }
+
+ public function titleize($string) {
+ $string = preg_replace('/[A-Z]/', ' $0', $string);
+ $string = str_replace('_', ' ', $string);
+ $string = ucwords($string);
+ return $string;
+ }
+
+}
+
+?>
48 core/models/mvc_database.php
@@ -0,0 +1,48 @@
+<?php
+
+class MvcDatabase {
+
+ private $wpdb;
+ private $debug = true;
+
+ function __construct() {
+ global $wpdb;
+ $this->wpdb = $wpdb;
+ $this->debug = MvcConfiguration::get('Debug');
+ }
+
+ public function get_results($string, $output_type=OBJECT) {
+ $this->add_to_log($string);
+ return $this->wpdb->get_results($string, $output_type);
+ }
+
+ public function get_var($string, $column_offset=0, $row_offset=0) {
+ $this->add_to_log($string);
+ return $this->wpdb->get_var($string, $column_offset, $row_offset);
+ }
+
+ public function query($string) {
+ $this->add_to_log($string);
+ return $this->wpdb->query($string);
+ }
+
+ public function escape($string) {
+ return mysql_real_escape_string($string);
+ }
+
+ public function escape_array($array) {
+ foreach($array as $key => $value) {
+ $array[$key] = $this->escape($value);
+ }
+ return $array;
+ }
+
+ private function add_to_log($string) {
+ if ($this->debug) {
+ echo '<pre>'.$string.'</pre>';
+ }
+ }
+
+}
+
+?>
193 core/models/mvc_database_adapter.php
@@ -0,0 +1,193 @@
+<?php
+
+class MvcDatabaseAdapter {
+
+ function __construct() {
+
+ $this->db = new MvcDatabase();
+
+ }
+
+ public function set_defaults($defaults) {
+ $this->defaults = $defaults;
+ }
+
+ public function query($sql) {
+ return $this->db->query($sql);
+ }
+
+ public function get_results($options_or_sql) {
+ if (is_array($options_or_sql)) {
+ $clauses = $this->get_sql_select_clauses($options_or_sql);
+ $sql = implode(' ', $clauses);
+ } else {
+ $sql = $options_or_sql;
+ }
+ return $this->db->get_results($sql);
+ }
+
+ public function get_var($sql) {
+ return $this->db->get_var($sql);
+ }
+
+ public function get_table_reference_sql($options=array()) {
+ $table_reference = empty($options['table_reference']) ? $this->defaults['table_reference'] : $options['table_reference'];
+ $table_alias = !isset($options['table_alias']) ? ' `'.$this->defaults['model_name'].'`' : $options['table_alias'];
+ return $table_reference.$table_alias;
+ }
+
+ public function get_sql_select_clauses($options=array()) {
+ $clauses = array(
+ 'select' => 'SELECT '.$this->get_select_sql($options),
+ 'from' => 'FROM '.$this->get_table_reference_sql($options),
+ 'joins' => $this->get_joins_sql($options),
+ 'where' => $this->get_where_sql($options),
+ 'order' => $this->get_order_sql($options),
+ 'limit' => $this->get_limit_sql($options),
+ );
+
+ return $clauses;
+ }
+
+ public function get_select_sql($options=array()) {
+ $selects = empty($options['selects']) ? $this->defaults['selects'] : $options['selects'];
+ if (!empty($options['additional_selects'])) {
+ $selects = array_merge($this->defaults['selects'], $options['additional_selects']);
+ }
+ return implode(', ', $selects);
+ }
+
+ public function get_joins_sql($options=array()) {
+ $joins = empty($options['joins']) ? $this->defaults['joins'] : $options['joins'];
+ if (empty($joins)) {
+ return '';
+ }
+ $clauses = array();
+ if (isset($joins['table'])) {
+ $joins = array($joins);
+ }
+ foreach($joins as $join) {
+ $type = empty($join['type']) ? 'JOIN' : $join['type'];
+ $clauses[] = $type.' '.$join['table'].' '.$join['alias'].' ON '.$join['on'];
+ }
+ return implode(' ', $clauses);
+ }
+
+ public function get_where_sql($options=array()) {
+ $conditions = empty($options['conditions']) ? $this->defaults['conditions'] : $options['conditions'];
+ if (empty($conditions)) {
+ return '';
+ }
+ if (is_array($conditions)) {
+ $sql_clauses = $this->get_where_sql_clauses($conditions, $options);
+ return 'WHERE '.implode(' AND ', $sql_clauses);
+ }
+ return 'WHERE '.$conditions;
+ }
+
+ public function get_where_sql_clauses($conditions, $options=array()) {
+ $use_table_alias = isset($options['use_table_alias']) ? $options['use_table_alias'] : true;
+ $sql_clauses = array();
+ foreach($conditions as $key => $value) {
+ if (is_array($value)) {
+ $clauses = $this->get_where_sql_clauses($value);
+ $logical_operator = $key == 'OR' ? ' OR ' : ' AND ';
+ $sql_clauses[] = '('.implode($logical_operator, $clauses).')';
+ continue;
+ }
+ if (strpos($key, '.') === false && $use_table_alias) {
+ $key = $this->defaults['model_name'].'.'.$key;
+ }
+ $operator = strpos($key, ' LIKE') === false ? ' = ' : ' ';
+ $sql_clauses[] = $this->db->escape($key).$operator.'"'.$this->db->escape($value).'"';
+ }
+ return $sql_clauses;
+ }
+
+ public function get_order_sql($options=array()) {
+ $order = empty($options['order']) ? $this->defaults['order'] : $options['order'];
+ return $order ? 'ORDER BY '.$order : '';
+ }
+
+ public function get_limit_sql($options=array()) {
+ if (!empty($options['page'])) {
+ $per_page = empty($options['per_page']) ? $this->defaults['per_page'] : $options['per_page'];
+ $page = $options['page'];
+ $offset = ($page - 1) * $per_page;
+ return 'LIMIT '.$offset.', '.$per_page;
+ }
+ $limit = empty($options['limit']) ? $this->defaults['limit'] : $options['limit'];
+ return $limit ? 'LIMIT '.$limit : '';
+ }
+
+ public function get_set_sql($data) {
+ $clauses = array();
+ foreach($data as $key => $value) {
+ if (is_string($value) || is_numeric($value)) {
+ $clauses[] = $key.' = "'.$this->db->escape($value).'"';
+ }
+ }
+ $sql = implode(', ', $clauses);
+ return $sql;
+ }
+
+ public function get_insert_columns_sql($data) {
+ $columns = array_keys($data);
+ $columns = $this->db->escape_array($columns);
+ $sql = '('.implode(', ', $columns).')';
+ return $sql;
+ }
+
+ public function get_insert_values_sql($data) {
+ $values = array();
+ foreach($data as $value) {
+ $values[] = '"'.$this->db->escape($value).'"';
+ }
+ $sql = '('.implode(', ', $values).')';
+ return $sql;
+ }
+
+ public function insert($data, $options=array()) {
+ $options['table_alias'] = false;
+ $options['use_table_alias'] = false;
+ if (empty($options['table_reference'])) {
+ // Filter out any data with a key that doesn't correspond to a column name in the table
+ $data = array_intersect_key($data, $this->schema);
+ }
+ $clauses = array(
+ 'insert' => 'INSERT INTO '.$this->get_table_reference_sql($options),
+ 'insert_columns' => $this->get_insert_columns_sql($data),
+ 'insert_values' => 'VALUES '.$this->get_insert_values_sql($data)
+ );
+ $sql = implode(' ', $clauses);
+ $this->query($sql);
+ $insert_id = mysql_insert_id();
+ return $insert_id;
+ }
+
+ public function update_all($data, $options=array()) {
+ $clauses = array(
+ 'update' => 'UPDATE '.$this->get_table_reference_sql($options),
+ 'set' => 'SET '.$this->get_set_sql($data),
+ 'where' => $this->get_where_sql($options),
+ 'limit' => $this->get_limit_sql($options)
+ );
+ $sql = implode(' ', $clauses);
+ $this->query($sql);
+ }
+
+ public function delete_all($options) {
+ $options['table_alias'] = false;
+ $options['use_table_alias'] = false;
+ $clauses = array(
+ 'update' => 'DELETE FROM '.$this->get_table_reference_sql($options),
+ 'where' => $this->get_where_sql($options),
+ 'limit' => $this->get_limit_sql($options)
+ );
+ $sql = implode(' ', $clauses);
+ $this->query($sql);
+ }
+
+}
+
+?>
521 core/models/mvc_model.php
@@ -0,0 +1,521 @@
+<?php
+
+class MvcModel {
+
+ public $table = null;
+ public $primary_key = 'id';
+ public $belongs_to = null;
+ public $has_many = null;
+ public $has_and_belongs_to_many = null;
+ public $associations = null;
+ public $admin_pages = null;
+ private $db_adapter = null;
+
+ function __construct() {
+
+ $this->name = preg_replace('/Model$/', '', get_class($this));
+
+ $table = empty($this->table) ? Inflector::tableize($this->name) : $this->table;
+
+ $defaults = array(
+ 'model_name' => $this->name,
+ 'table' => $table,
+ 'table_reference' => empty($this->database) ? '`'.$table.'`' : '`'.$this->database.'`.`'.$table.'`',
+ 'selects' => empty($this->selects) ? array('`'.$this->name.'`.*') : $this->default_selects,
+ 'order' => empty($this->order) ? null : $this->order,
+ 'joins' => empty($this->joins) ? null : $this->joins,
+ 'conditions' => empty($this->conditions) ? null : $this->conditions,
+ 'limit' => empty($this->limit) ? null : $this->limit,
+ 'includes' => empty($this->includes) ? null : $this->includes,
+ 'per_page' => empty($this->per_page) ? 10 : $this->per_page
+ );
+
+ foreach($defaults as $key => $value) {
+ $this->{$key} = $value;
+ }
+
+ $this->db_adapter = new MvcDatabaseAdapter();
+ $this->db_adapter->set_defaults($defaults);
+
+ $this->init_admin_pages();
+ $this->init_admin_columns();
+ $this->init_associations();
+ $this->init_schema();
+
+ }
+
+ public function create($data) {
+ if (empty($data[$this->name])) {
+ return false;
+ }
+ $model_data = $data[$this->name];
+ $id = $this->insert($model_data);
+ $this->update_associations($id, $model_data);
+ }
+
+ public function save($data) {
+ if (empty($data[$this->name])) {
+ return false;
+ }
+ if (!empty($data[$this->name]['id'])) {
+ $model_data = $data[$this->name];
+ $id = $model_data['id'];
+ unset($model_data['id']);
+ $this->update($id, $model_data);
+ $this->update_associations($id, $model_data);
+ } else {
+ $this->create($data);
+ }
+ }
+
+ public function insert($data) {
+ $insert_id = $this->db_adapter->insert($data);
+ $this->insert_id = $insert_id;
+ return $insert_id;
+ }
+
+ public function update($id, $data) {
+ $options = array(
+ 'conditions' => array($this->name.'.'.$this->primary_key => $id)
+ );
+ $this->db_adapter->update_all($data, $options);
+ }
+
+ public function update_all($data, $options=array()) {
+ $this->db_adapter->update_all($data, $options);
+ }
+
+ public function delete($id) {
+ $options = array(
+ 'conditions' => array($this->primary_key => $id)
+ );
+ $this->db_adapter->delete_all($options);
+ }
+
+ public function delete_all($options=array()) {
+ $this->db_adapter->delete_all($options);
+ }
+
+ public function find($options=array()) {
+ $options = $this->process_find_options($options);
+ $objects = $this->db_adapter->get_results($options);
+ $objects = $this->process_objects($objects, $options);
+ return $objects;
+ }
+
+ public function find_by_id($id, $options=array()) {
+ $options = $this->process_find_options($options);
+ $options['conditions'] = array($this->name.'.'.$this->primary_key => $id);
+ $object = $this->db_adapter->get_results($options);
+ $object = isset($object[0]) ? $object[0] : null;
+ $object = $this->process_objects($object, $options);
+ return $object;
+ }
+
+ public function paginate($options=array()) {
+ $options = $this->process_find_options($options);
+ $options['page'] = empty($options['page']) ? 1 : intval($options['page']);
+ $options['per_page'] = empty($options['per_page']) ? $this->per_page : intval($options['per_page']);
+ $objects = $this->db_adapter->get_results($options);
+ $objects = $this->process_objects($objects, $options);
+ $total_count = $this->get_total_count($options);
+ $response = array(
+ 'objects' => $objects,
+ 'total_pages' => ceil($total_count/$options['per_page']),
+ 'page' => $options['page']
+ );
+ return $response;
+
+ }
+
+ protected function get_total_count($options=array()) {
+ $clauses = $this->db_adapter->get_sql_select_clauses($options);
+ $clauses['select'] = 'SELECT COUNT(*) AS count';
+ unset($clauses['limit']);
+ $sql = implode(' ', $clauses);
+ $result = $this->db_adapter->get_var($sql);
+ return $result;
+ }
+
+ private function process_find_options($options) {
+ if (!empty($options['joins'])) {
+ if (is_string($options['joins'])) {
+ $options['joins'] = array($options['joins']);
+ }
+ foreach($options['joins'] as $key => $join) {
+ if (is_string($join)) {
+ $join_model_name = $join;
+ if (!empty($this->associations[$join_model_name])) {
+ $association = $this->associations[$join_model_name];
+
+ $join_model = new $join_model_name();
+
+ switch ($association['type']) {
+ case 'belongs_to':
+ $join = array(
+ 'table' => $join_model->table,
+ 'on' => $join_model_name.'.'.$join_model->primary_key.' = '.$this->name.'.'.$association['foreign_key'],
+ 'alias' => $join_model_name
+ );
+ break;
+
+ case 'has_many':
+ // To do: test this
+ $join = array(
+ 'table' => $this->table,
+ 'on' => $join_model_name.'.'.$association['foreign_key'].' = '.$this->name.'.'.$this->primary_key,
+ 'alias' => $join_model_name
+ );
+ break;
+
+ case 'has_and_belongs_to_many':
+ $join_table_alias = $join_model_name.$this->name;
+ // The join for the HABTM join table
+ $join = array(
+ 'table' => $association['join_table'],
+ 'on' => $join_table_alias.'.'.$association['foreign_key'].' = '.$this->name.'.'.$this->primary_key,
+ 'alias' => $join_table_alias
+ );
+ // The join for the association model's table
+ $second_join = array(
+ 'table' => $join_model->table,
+ 'on' => $join_table_alias.'.'.$association['association_foreign_key'].' = '.$join_model_name.'.'.$join_model->primary_key,
+ 'alias' => $join_model_name
+ );
+ $options['joins'][] = $second_join;
+ break;
+ }
+ }
+ $options['joins'][$key] = $join;
+ }
+ }
+ }
+ return $options;
+ }
+
+ private function update_associations($id, $model_data) {
+ if (!empty($this->associations)) {
+ foreach($this->associations as $association) {
+ switch($association['type']) {
+ case 'has_many':
+ $this->update_has_many_associations($id, $association, $model_data);
+ break;
+ case 'has_and_belongs_to_many':
+ $this->update_has_and_belongs_to_many_associations($id, $association, $model_data);
+ break;
+ }
+ }
+ }
+ }
+
+ private function update_has_many_associations($object_id, $association, $model_data) {
+ $association_name = $association['name'];
+ if (!empty($model_data[$association_name])) {
+ if (isset($model_data[$association_name]['ids'])) {
+ if (!empty($model_data[$association_name]['ids'])) {
+ // To do: Implement this by first emptying the foreign key values of associated records
+ // that currently equal $object_id, then loop through 'ids', setting those foreign key
+ // values.
+ }
+ }
+ }
+ }
+
+ private function update_has_and_belongs_to_many_associations($object_id, $association, $model_data) {
+ $association_name = $association['name'];
+ if (!empty($model_data[$association_name])) {
+ if (isset($model_data[$association_name]['ids'])) {
+ $this->db_adapter->delete_all(array(
+ 'table_reference' => $association['join_table'],
+ 'conditions' => array($association['foreign_key'] => $object_id)
+ ));
+ if (!empty($model_data[$association_name]['ids'])) {
+ foreach($model_data[$association_name]['ids'] as $association_id) {
+ if (!empty($association_id)) {
+ $this->db_adapter->insert(
+ array(
+ $association['foreign_key'] => $object_id,
+ $association['association_foreign_key'] => $association_id,
+ ),
+ array(
+ 'table_reference' => $association['join_table']
+ )
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private function init_admin_pages() {
+ $titleized = Inflector::titleize($this->name);
+ $default_pages = array(
+ 'add' => array(
+ 'label' => 'Add New'
+ ),
+ 'delete' => array(
+ 'label' => 'Delete '.$titleized,
+ 'in_menu' => false
+ ),
+ 'edit' => array(
+ 'label' => 'Edit '.$titleized,
+ 'in_menu' => false
+ )
+ );
+ if (!isset($this->admin_pages)) {
+ $this->admin_pages = $default_pages;
+ }
+ $admin_pages = array();
+ foreach($this->admin_pages as $key => $value) {
+ if (is_int($key)) {
+ $key = $value;
+ $value = array();
+ }
+ $defaults = array(
+ 'action' => $key,
+ 'in_menu' => true,
+ 'label' => Inflector::titleize($key),
+ 'capability' => 'administrator'
+ );
+ if (isset($default_pages[$key])) {
+ $value = array_merge($default_pages[$key], $value);
+ }
+ $value = array_merge($defaults, $value);
+ $admin_pages[$key] = $value;
+ }
+ $this->admin_pages = $admin_pages;
+ }
+
+ private function init_admin_columns() {
+ $admin_columns = array();
+ foreach($this->admin_columns as $key => $value) {
+ if (is_array($value)) {
+ if (!isset($value['label'])) {
+ $value['label'] = Inflector::titleize($key);
+ }
+ } else if (is_integer($key)) {
+ $key = $value;
+ if ($value == 'id') {
+ $value = array('label' => 'ID');
+ } else {
+ $value = array('label' => Inflector::titleize($value));
+ }
+ } else {
+ $value = array('label' => $value);
+ }
+ $value['key'] = $key;
+ $admin_columns[$key] = $value;
+ }
+ $this->admin_columns = $admin_columns;
+ }
+
+ private function process_objects($objects, $options=array()) {
+ if (!is_array($objects) && !is_object($objects)) {
+ return null;
+ }
+ $single_object = false;
+ if (is_object($objects)) {
+ $objects = array($objects);
+ $single_object = true;
+ }
+
+ $includes = array_key_exists('includes', $options) ? $options['includes'] : $this->includes;
+
+ if (is_string($includes)) {
+ $includes = array($includes);
+ }
+
+ if (!empty($includes)) {
+ // Instantiate associated models, so that they don't need to be instantiated multiple times in the subsequent for loop
+ $models = array();
+ foreach($includes as $key => $include) {
+ $model_name = is_string($include) ? $include : $key;
+ $models[$model_name] = new $model_name();
+ }
+ }
+
+ $recursive = isset($options['recursive']) ? $options['recursive'] - 1 : 2;
+
+ foreach($objects as $key => $object) {
+
+ if (!empty($this->primary_key)) {
+ $object->__id = $object->{$this->primary_key};
+ }
+
+ if (!empty($includes) && $recursive != 0) {
+ foreach($includes as $include_key => $include) {
+ if (is_string($include)) {
+ $model_name = $include;
+ $association = $this->associations[$model_name];
+ } else {
+ $model_name = $include_key;
+ $association = $include;
+ if (!empty($this->associations[$model_name])) {
+ if (!empty($association['selects'])) {
+ if (is_string($association['selects'])) {
+ $association['selects'] = array($association['selects']);
+ }
+ $association['fields'] = $association['selects'];
+ }
+ $association = array_merge($this->associations[$model_name], $association);
+ }
+ }
+ if (empty($association['fields'])) {
+ $association['fields'] = array($association['name'].'.*');
+ }
+ $model = $models[$model_name];
+ switch ($association['type']) {
+ case 'belongs_to':
+ $associated_object = $model->find_by_id($object->{$association['foreign_key']}, array(
+ 'recursive' => $recursive
+ ));
+ $object->{Inflector::underscore($model_name)} = $associated_object;
+ break;
+
+ case 'has_many':
+ $associated_objects = $model->find(array(
+ 'selects' => $association['fields'],
+ 'conditions' => array($association['foreign_key'] => $object->__id),
+ 'recursive' => $recursive
+ ));
+ $object->{Inflector::tableize($model_name)} = $associated_objects;
+ break;
+
+ case 'has_and_belongs_to_many':
+ $join_alias = 'JoinTable';
+ $associated_objects = $model->find(array(
+ 'selects' => $association['fields'],
+ 'joins' => array(
+ 'table' => $association['join_table'],
+ 'on' => $join_alias.'.'.$association['association_foreign_key'].' = '.$model_name.'.'.$model->primary_key,
+ 'alias' => $join_alias
+ ),
+ 'conditions' => array($join_alias.'.'.$association['foreign_key'] => $object->__id),
+ 'recursive' => $recursive
+ ));
+ $object->{Inflector::tableize($model_name)} = $associated_objects;
+ break;
+ }
+ }
+ }
+
+ if (method_exists($this, 'after_find')) {
+ $this->after_find($object);
+ }
+
+ // Set this after after_find, in case after_find sets this field
+ if (!empty($this->display_field)) {
+ $object->__name = empty($object->{$this->display_field}) ? null : $object->{$this->display_field};
+ }
+
+ $objects[$key] = $object;
+ }
+ if ($single_object) {
+ return $objects[0];
+ }
+ return $objects;
+ }
+
+ protected function init_schema() {
+ $sql = '
+ DESCRIBE
+ '.$this->table;
+ $results = $this->db_adapter->get_results($sql);
+