├── .gitmodules ├── Cm_Mongo.xml ├── LICENSE.txt ├── README.md ├── code ├── Block │ ├── Adminhtml │ │ ├── Enum.php │ │ ├── Enum │ │ │ ├── Edit.php │ │ │ ├── Edit │ │ │ │ ├── Form.php │ │ │ │ ├── Values.php │ │ │ │ └── Values │ │ │ │ │ └── Value.php │ │ │ └── Grid.php │ │ └── Widget │ │ │ └── Grid │ │ │ └── Column │ │ │ └── Renderer │ │ │ ├── Datestring.php │ │ │ ├── Datetime.php │ │ │ └── Implode.php │ └── Profiler.php ├── Collection.php ├── Filter.php ├── Getter.php ├── Helper │ ├── Data.php │ └── Queue.php ├── Model │ ├── Abstract.php │ ├── Entity.php │ ├── Enum.php │ ├── Fixture.php │ ├── Indexer.php │ ├── Indexer │ │ └── Schema.php │ ├── Job.php │ ├── Mongo │ │ ├── Enum.php │ │ ├── Enum │ │ │ └── Collection.php │ │ ├── Fixture.php │ │ ├── Indexer.php │ │ ├── Job.php │ │ └── Job │ │ │ └── Collection.php │ ├── Resource │ │ ├── Abstract.php │ │ ├── Collection │ │ │ ├── Abstract.php │ │ │ └── Embedded.php │ │ ├── Setup.php │ │ └── Type │ │ │ ├── Mongo.php │ │ │ └── Shim.php │ ├── Schema.php │ └── Type │ │ ├── Interface.php │ │ ├── Tomongo.php │ │ └── Tophp.php ├── Profiler.php ├── controllers │ └── Adminhtml │ │ └── EnumController.php └── etc │ ├── adminhtml.xml │ ├── config.xml │ ├── mongo.xml │ ├── mongo.xml.sample │ ├── mongo_indexer.xml.sample │ └── system.xml ├── composer.json ├── layout └── mongo.xml └── modman /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/mongodb-php-odm"] 2 | path = lib/mongodb-php-odm 3 | url = git://github.com/colinmollenhour/mongodb-php-odm.git 4 | -------------------------------------------------------------------------------- /Cm_Mongo.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | true 9 | community 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, Colin Mollenhour 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * The name of Colin Mollenhour may not be used to endorse or promote products 13 | derived from this software without specific prior written permission. 14 | * The Cm_Mongo namespace used by this module may not be modified. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cm_Mongo 2 | ======== 3 | 4 | NOTE: This is NOT a drop-in replacement for MySQL. It is intended only for use in implementing new models on MongoDb. 5 | ----------- 6 | 7 | NOTE: PHP 7 Support 8 | ------------- 9 | 10 | PHP 7 is not supported by the legacy 'mongo' extension and I currently have no plans to update this to use the new 'mongodb' extension which is not API compatible. I will happily accept a pull rquest though! :) 11 | 12 | Description 13 | ----------- 14 | 15 | This Magento extension is primarily two things: 16 | 17 | * **An abstraction layer for MongoDb.** Like the MySQL abstraction layer included in Magento, it provides 18 | classes that extend `Mage_Core_Model_Abstract` and `Varien_Data_Collection`. It also includes support for 19 | .js upgrade scripts, an indexer to automatically index fields in other collections, and a schema 20 | defined by XML files. 21 | 22 | * **An atomic job queue.** The job queue can be run by the Magento cron or by a separate script and supports 23 | future execution dates, automatic retry intervals, disabling jobs by name or group name, and priorities. 24 | 25 | See [mongo.xml.sample](code/etc/mongo.xml.sample) and [Job.php](code/Model/Job.php) for more information. 26 | 27 | Unit Testing 28 | ------------ 29 | 30 | Cm_Mongo can be used with [EcomDev_PhpUnit](https://github.com/IvanChepurnyi/EcomDev_PHPUnit), 31 | but needs to override one of it's classes for fixtures with mongo collections to work. Add a module 32 | dependency on EcomDev_PhpUnit to Cm_Mongo.xml when using with EcomDev_PhpUnit. 33 | 34 | 35 | License 36 | ------- 37 | 38 | Please see the terms defined in LICENSE.txt 39 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Enum.php: -------------------------------------------------------------------------------- 1 | _blockGroup = 'mongo'; 9 | $this->_controller = 'adminhtml_enum'; 10 | $this->_headerText = $this->__('Manage Enumerations'); 11 | parent::__construct(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Enum/Edit.php: -------------------------------------------------------------------------------- 1 | _controller = 'adminhtml_enum'; 9 | $this->_blockGroup = 'mongo'; 10 | 11 | parent::__construct(); 12 | 13 | $this->_updateButton('save', 'label', $this->__('Save Enumeration')); 14 | $this->_updateButton('delete', 'label', $this->__('Delete Enumeration')); 15 | 16 | $this->setData('form_action_url', $this->getUrl('*/*/save')); 17 | } 18 | 19 | public function getHeaderText() 20 | { 21 | $model = Mage::registry('edit_model'); 22 | if($model->getId()) { 23 | return $this->__('Edit %s', $model->getName()); 24 | } else { 25 | return $this->__('New Enumeration'); 26 | } 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Enum/Edit/Form.php: -------------------------------------------------------------------------------- 1 | 'edit_form', 10 | 'action' => $this->getData('action'), 11 | 'method' => 'post', 12 | 'use_container' => TRUE 13 | )); 14 | 15 | $fieldset = $form->addFieldset('main_fieldset', array( 16 | 'legend' => $this->__('Enumeration Info'), 17 | )); 18 | 19 | $model = Mage::registry('edit_model'); 20 | 21 | $idField = $fieldset->addField('_id', 'text', array( 22 | 'name' => '_id', 23 | 'label' => $this->__('Identifier'), 24 | 'required' => TRUE, 25 | )); 26 | 27 | if($model->getId()) { 28 | $idField->setReadonly(TRUE); 29 | } 30 | 31 | $fieldset->addField('name', 'text', array( 32 | 'name' => 'name', 33 | 'label' => $this->__('Name'), 34 | 'required' => TRUE, 35 | )); 36 | 37 | $field = $fieldset->addField('defaults', 'text', array( 38 | 'name' => 'defaults', 39 | 'label' => $this->__('Default Values'), 40 | )); 41 | 42 | $defaultsRenderer = $this->getLayout()->createBlock('mongo/adminhtml_enum_edit_values', 'values'); 43 | $defaultsRenderer->setDisableExisting(!!$model->getId()); 44 | $field->setRenderer($defaultsRenderer); 45 | 46 | $formData = $model->getData(); 47 | 48 | //var_dump($formData); die; 49 | 50 | $defaults = array(); 51 | if( ! empty($formData['defaults'])) { 52 | foreach($formData['defaults'] as $key => $value) { 53 | $value['value'] = $key; 54 | $defaults[$key] = $value; 55 | } 56 | } 57 | $formData['defaults'] = $defaults; 58 | 59 | $form->setValues($formData); 60 | 61 | $this->setForm($form); 62 | return parent::_prepareForm(); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Enum/Edit/Values.php: -------------------------------------------------------------------------------- 1 | _addButtonLabel = $this->__('Add Value'); 9 | 10 | $valueRenderer = Mage::app()->getLayout()->createBlock('mongo/adminhtml_enum_edit_values_value'); 11 | $valueColumn = $this->addColumn('value', array( 12 | 'label' => $this->__('Identifier'), 13 | 'style' => 'width:120px;', 14 | 'renderer' => $valueRenderer, 15 | )); 16 | if($this->getDisableExisting()) { 17 | $this->_columns['value']['disabled'] = true; 18 | } 19 | 20 | $this->addColumn('label', array( 21 | 'label' => $this->__('Label'), 22 | 'style' => 'width:120px;', 23 | )); 24 | } 25 | 26 | protected function _toHtml() 27 | { 28 | $html = parent::_toHtml(); 29 | $html .= ''; 30 | return $html; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Enum/Edit/Values/Value.php: -------------------------------------------------------------------------------- 1 | getInputName(); 9 | $columnName = $this->getColumnName(); 10 | $column = $this->getColumn(); 11 | 12 | return ''; 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /code/Block/Adminhtml/Enum/Grid.php: -------------------------------------------------------------------------------- 1 | setId('mongoEnums'); 10 | //$this->setDefaultSort('_id'); 11 | } 12 | 13 | public function _prepareCollection() 14 | { 15 | $this->setCollection(Mage::getResourceModel('mongo/enum_collection')); 16 | parent::_prepareCollection(); 17 | } 18 | 19 | public function _prepareColumns() 20 | { 21 | $this->addColumn('name', array( 22 | 'header' => $this->__('Name'), 23 | 'index' => 'name', 24 | )); 25 | 26 | $this->addColumn('_id', array( 27 | 'header' => $this->__('ID'), 28 | 'index' => '_id', 29 | )); 30 | 31 | $this->addColumn('values_count', array( 32 | 'header' => $this->__('Values'), 33 | 'getter' => Cm_Mongo_Getter::factory('getValuesCount'), 34 | 'width' => '100px', 35 | 'align' => 'right', 36 | 'sortable' => false, 37 | 'filter' => false, 38 | )); 39 | 40 | parent::_prepareColumns(); 41 | } 42 | 43 | public function getRowUrl($row) 44 | { 45 | return $this->getUrl('*/*/edit', array('id' => $row->getId())); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Widget/Grid/Column/Renderer/Datestring.php: -------------------------------------------------------------------------------- 1 | getColumn()->getFormat(); 23 | if (!$format) { 24 | if (is_null(self::$_format)) { 25 | try { 26 | self::$_format = Mage::app()->getLocale()->getDateFormat( 27 | Mage_Core_Model_Locale::FORMAT_TYPE_MEDIUM 28 | ); 29 | } 30 | catch (Exception $e) { 31 | 32 | } 33 | } 34 | $format = self::$_format; 35 | } 36 | return $format; 37 | } 38 | 39 | /** 40 | * Renders grid column 41 | * 42 | * @param Varien_Object $row 43 | * @return string 44 | */ 45 | public function render(Varien_Object $row) 46 | { 47 | if ($data = $this->_getValue($row)) { 48 | $format = $this->_getFormat(); 49 | $data = Mage::app()->getLocale()->date($data, Varien_Date::DATE_INTERNAL_FORMAT, NULL, FALSE)->toString($format); 50 | return $data; 51 | } 52 | return $this->getColumn()->getDefault(); 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Widget/Grid/Column/Renderer/Datetime.php: -------------------------------------------------------------------------------- 1 | _getValue($row)) 17 | { 18 | try { 19 | if($data instanceof MongoDate) { 20 | $date = new Zend_Date($data->sec, null, Mage::app()->getLocale()->getLocale()); 21 | } 22 | else if($data instanceof Zend_Date) { 23 | $date = $data; 24 | $date->setLocale(Mage::app()->getLocale()->getLocale()); 25 | } 26 | else if(is_int($data) || is_float($data) || ctype_digit($data)) { 27 | $date = new Zend_Date($data, null, Mage::app()->getLocale()->getLocale()); 28 | } 29 | else { 30 | $date = Mage::app()->getLocale()->date($data, Varien_Date::DATETIME_INTERNAL_FORMAT); 31 | } 32 | if ($timezone = Mage::app()->getStore()->getConfig(Mage_Core_Model_Locale::XML_PATH_DEFAULT_TIMEZONE)) { 33 | $date->setTimezone($timezone); 34 | } 35 | $data = $date->toString($this->_getFormat()); 36 | } 37 | catch (Exception $e) 38 | { 39 | $data = $this->getColumn()->getDefault(); 40 | } 41 | return $data; 42 | } 43 | return $this->getColumn()->getDefault(); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /code/Block/Adminhtml/Widget/Grid/Column/Renderer/Implode.php: -------------------------------------------------------------------------------- 1 | getColumn()->getPluck()) { 14 | $array = array(); 15 | foreach($value as $_value) { 16 | if(isset($_value[$pluck])) { 17 | $array[] = $_value[$pluck]; 18 | } 19 | } 20 | } else { 21 | $array = $value; 22 | } 23 | $glue = $this->getColumn()->getGlue() ? $this->getColumn()->getGlue() : '
'; 24 | return implode($glue, $array); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /code/Block/Profiler.php: -------------------------------------------------------------------------------- 1 | _beforeToHtml() 9 | || !Mage::getStoreConfigFlag('dev/debug/mongo_profiler') 10 | || !Mage::helper('core')->isDevAllowed()) { 11 | return ''; 12 | } 13 | 14 | $timers = Cm_Mongo_Profiler::getTimers(); 15 | $allSum = 0.0; 16 | $allCount = 0; 17 | 18 | ob_start(); 19 | echo '
'; 20 | echo ''; 21 | echo "[mongo profiler]"; 22 | echo ''; 23 | echo ''; 24 | foreach ($timers as $name=>$timer) { 25 | $sum = Cm_Mongo_Profiler::fetch($name,'sum'); 26 | $count = Cm_Mongo_Profiler::fetch($name,'count'); 27 | $allSum += $sum; 28 | $allCount += $count; 29 | echo '' 30 | .'' 31 | .'' 32 | .'' 33 | .'' 34 | ; 35 | } 36 | echo ''; 37 | echo '
QueryTimeCount
'.$name.''.number_format($sum,4).''.$count.'
Total'.number_format($allSum,4).''.$allCount.'
'; 38 | echo '
'; 39 | 40 | return ob_get_clean(); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /code/Collection.php: -------------------------------------------------------------------------------- 1 | _getItemKey($item->getId()); 17 | 18 | if ($itemId !== NULL) { 19 | if (isset($this->_items[$itemId])) { 20 | throw new Exception('Item ('.get_class($item).') with the same id "'.$item->getId().'" already exist'); 21 | } 22 | $this->_items[$itemId] = $item; 23 | } else { 24 | $this->_items[] = $item; 25 | } 26 | return $this; 27 | } 28 | 29 | /** 30 | * Remove item from collection by item key 31 | * 32 | * @param mixed $key 33 | * @return Cm_Mongo_Collection 34 | */ 35 | public function removeItemByKey($key) 36 | { 37 | $key = $this->_getItemKey($key); 38 | if (isset($this->_items[$key])) { 39 | unset($this->_items[$key]); 40 | } 41 | return $this; 42 | } 43 | 44 | /** 45 | * Get a key for an item 46 | * 47 | * @param mixed $id 48 | * @return mixed 49 | */ 50 | protected function _getItemKey($id) 51 | { 52 | if($id instanceof MongoId) { 53 | return (string) $id; 54 | } 55 | return $id; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /code/Filter.php: -------------------------------------------------------------------------------- 1 | Cm_Mongo_Filter::factory( 20 | * 'foo/bar_collection', 21 | * 'org_id', 22 | * array( 23 | * 'status' => array('$ne' => 'inactive'), 24 | * ) 25 | * ), 26 | * 'filter_index' => 'name', 27 | * 28 | * @param string $resource Resource name or collection instance 29 | * @param string $field Field name to apply condition to 30 | * @param string $filters Optional filters to apply before conditions 31 | * @return array 32 | */ 33 | public static function factory($resource, $field, $filters = NULL) 34 | { 35 | $filter = new self($resource, $field, $filters); 36 | return array($filter, 'apply'); 37 | } 38 | 39 | /** 40 | * Recommended to instead use static factory method. 41 | * 42 | * @see Cm_Mongo_Filter::factory() 43 | * 44 | * @param string $resource Resource name or collection instance 45 | * @param string $field Field name to apply condition to 46 | * @param string $filters Optional filters to apply before conditions 47 | */ 48 | public function __construct($resource, $field, $filters = NULL) 49 | { 50 | $this->_resource = $resource; 51 | $this->_field = $field; 52 | $this->_filters = $filters; 53 | } 54 | 55 | /** 56 | * Apply the condition to the given collection by way of the resource specified on instantiation 57 | * 58 | * @param \Cm_Mongo_Model_Resource_Collection_Abstract $collection 59 | * @param \Mage_Adminhtml_Block_Widget_Grid_Column $column 60 | */ 61 | public function apply(Cm_Mongo_Model_Resource_Collection_Abstract $collection, Mage_Adminhtml_Block_Widget_Grid_Column $column) 62 | { 63 | if(is_string($this->_resource)) { 64 | $filterCollection = Mage::getResourceModel($this->_resource); 65 | } 66 | else { 67 | $filterCollection = $this->_resource; 68 | } 69 | 70 | var_dump($filterCollection); 71 | if($this->_filters) { 72 | $filterCollection->addFieldToFilter($this->_filters); 73 | } 74 | 75 | $field = $column->getFilterIndex() ? $column->getFilterIndex() : $column->getIndex(); 76 | $cond = $column->getFilter()->getCondition(); 77 | if ($field && isset($cond)) { 78 | $filterCollection->addFieldToFilter($field , $cond); 79 | } 80 | $collection->addFieldToFilter($this->_field, '$in', $filterCollection->getAllIds(TRUE)); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /code/Getter.php: -------------------------------------------------------------------------------- 1 | Cm_Mongo_Getter::factory('getFoo.getBar'), 18 | * 'getter' => Cm_Mongo_Getter::factory('getFoo.getUpdatedAt', 'timestamp'), 19 | * 20 | * @param string $path Dot-delimited path to desired data 21 | * @param string $type Optional data type to convert value to 22 | * @return array 23 | */ 24 | public static function factory($path, $type = NULL) 25 | { 26 | $getter = new self($path, $type); 27 | return array($getter, $type === NULL ? 'fetch' : 'fetchAsType'); 28 | } 29 | 30 | /** 31 | * Recommended to instead use static factory method. 32 | * 33 | * @see Cm_Mongo_Getter::factory() 34 | * 35 | * @param string $path Dot-delimited path to desired data 36 | * @param string $type Optional data type to convert value to 37 | */ 38 | public function __construct($path, $type = NULL) 39 | { 40 | $this->_parts = explode('.',$path); 41 | $this->_type = $type; 42 | } 43 | 44 | /** 45 | * Get a nested value from an object using the getter's path. 46 | * 47 | * @param Varien_Object $object 48 | * @return mixed 49 | */ 50 | public function fetch(Varien_Object $object) 51 | { 52 | for($value = $object, $i = 0; $i < count($this->_parts); $i++) 53 | { 54 | $value = $value->{$this->_parts[$i]}(); 55 | } 56 | return $value; 57 | } 58 | 59 | /** 60 | * Fetch the value and convert the type using the tophp converter. 61 | * 62 | * @param Varien_Object $object 63 | * @return mixed 64 | */ 65 | public function fetchAsType(Varien_Object $object) 66 | { 67 | $value = $this->fetch($object); 68 | return Mage::getSingleton('mongo/type_tophp')->{$this->_type}(NULL, $value); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /code/Helper/Data.php: -------------------------------------------------------------------------------- 1 | 'mongo/adminhtml_widget_grid_column_renderer_datestring', 16 | 'datetime' => 'mongo/adminhtml_widget_grid_column_renderer_datetime', 17 | 'implode' => 'mongo/adminhtml_widget_grid_column_renderer_implode', 18 | ); 19 | if($key !== NULL) { 20 | return isset($renderers[$key]) ? $renderers[$key] : NULL; 21 | } 22 | return $renderers; 23 | } 24 | 25 | /** 26 | * Render the profiler contents as HTML 27 | * 28 | * @return string 29 | */ 30 | public function getProfilerHtml() 31 | { 32 | return Mage::app()->getLayout()->createBlock('mongo/profiler')->toHtml(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /code/Helper/Queue.php: -------------------------------------------------------------------------------- 1 | _initNewJob($task, $data, $priority, $executeAt); 20 | $job->save(); 21 | return $job; 22 | } 23 | 24 | /** 25 | * Add a job to the queue if it isn't already ready in the queue (upsert) 26 | * 27 | * @param string $task 28 | * @param array $data 29 | * @param int $priority Optional override for priority 30 | * @param int|MongoDate $executeAt Optional timestamp specifying when to be run 31 | * @return Cm_Mongo_Model_Job 32 | */ 33 | public function addJobUnique($task, $data, $priority = null, $executeAt = null) 34 | { 35 | $job = $this->_initNewJob($task, $data, $priority, $executeAt); 36 | $job->isObjectNew(NULL); // force upsert 37 | $job->setAdditionalSaveCriteria(array( 38 | 'status' => Cm_Mongo_Model_Job::STATUS_READY, 39 | 'task' => $task, 40 | 'job_data' => $data, 41 | )); 42 | $job->save(); 43 | return $job; 44 | } 45 | 46 | /** 47 | * Add a job to the queue in a disabled state with a known _id, to be used in two-phase commits 48 | * 49 | * Enable the jobs using Mage::getResourceSingleton('mongo/job')->enableJobs($query); 50 | * 51 | * @param MongoId $id 52 | * @param string $task 53 | * @param array $data 54 | * @param int $priority Optional override for priority 55 | * @param int|MongoDate $executeAt Optional timestamp specifying when to be run 56 | * @return Cm_Mongo_Model_Job 57 | */ 58 | public function addJobIdempotent(MongoId $id, $task, $data, $priority = null, $executeAt = null) 59 | { 60 | $job = $this->_initNewJob($task, $data, $priority, $executeAt); 61 | $job->setId($id); 62 | $job->setStatus(Cm_Mongo_Model_Job::STATUS_DISABLED); 63 | $job->isObjectNew(NULL); // force upsert 64 | $job->save(); 65 | return $job; 66 | } 67 | 68 | /** 69 | * Add a job to the queue and attempt to run it immediately. 70 | * 71 | * If it was already picked up by the queue it will not be run but no error will be reported. 72 | * 73 | * @param string $task 74 | * @param array $data 75 | * @param boolean $throws 76 | * @return boolean 77 | */ 78 | public function addAndRunJob($task, $data, $throws = FALSE) 79 | { 80 | try { 81 | $job = $this->addJob($task, $data); 82 | if( $job->getResource()->reloadJobForRunning($job) ) { 83 | $job->run(); 84 | } 85 | } 86 | catch(Exception $e) { 87 | if($throws) { 88 | throw $e; 89 | } else { 90 | Mage::logException($e); 91 | return FALSE; 92 | } 93 | } 94 | return TRUE; 95 | } 96 | 97 | /** 98 | * Run the next jobs in the queue until the limit is reached 99 | * 100 | * @param int|null $limit Maximum number of jobs to run 101 | * @param int|null $timeLimit Maximum amount of time to run for 102 | * @return int the number of jobs executed 103 | */ 104 | public function runQueue($limit = 100, $timeLimit = null) 105 | { 106 | if($stopTime = $timeLimit) { 107 | $stopTime += time(); 108 | } 109 | for($i = 0; ( ! $limit || $i < $limit) && ( ! $stopTime || time() < $stopTime); $i++) 110 | { 111 | $job = Mage::getResourceSingleton('mongo/job')->getNextJob(); /* @var $job Cm_Mongo_Model_Job */ 112 | if( ! $job) { 113 | break; 114 | } 115 | $job->run(); 116 | } 117 | return $i; 118 | } 119 | 120 | /** 121 | * Initialize a new job instance 122 | * 123 | * @param string $task 124 | * @param array $data 125 | * @param int $priority Optional override for priority 126 | * @param int|MongoDate $executeAt Optional timestamp specifying when to be run 127 | * @return Cm_Mongo_Model_Job 128 | */ 129 | protected function _initNewJob($task, $data, $priority = null, $executeAt = null) 130 | { 131 | $job = Mage::getModel('mongo/job')->setData(array( 132 | 'status' => Cm_Mongo_Model_Job::STATUS_READY, 133 | 'task' => $task, 134 | 'job_data' => $data, 135 | 'priority' => $priority, 136 | 'execute_at' => $executeAt === null ? new MongoDate : $executeAt, 137 | )); 138 | return $job; 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /code/Model/Abstract.php: -------------------------------------------------------------------------------- 1 | _isObjectNew = $flag; 42 | } 43 | return $this->_isObjectNew; 44 | } 45 | 46 | /** 47 | * Get some or all data. 48 | * 49 | * Overridden to allow . delimited names to retrieve nested data and disable the second parameter. 50 | * 51 | * @param string $key 52 | * @param int $index DO NOT USE 53 | * @return mixed 54 | */ 55 | public function getData($key='', $index=null) 56 | { 57 | if(''===$key) { 58 | return $this->_data; 59 | } 60 | if(strpos($key,'.')) { 61 | $keyArr = explode('.', $key); 62 | $data = $this->_data; 63 | foreach($keyArr as $k) { 64 | if($data instanceof Varien_Object) { 65 | $data = $data->getData($k); 66 | } 67 | else if(is_array($data)) { 68 | if( ! isset($data[$k])) { 69 | return NULL; 70 | } 71 | $data = $data[$k]; 72 | } 73 | else { 74 | return NULL; 75 | } 76 | } 77 | return $data; 78 | } 79 | return isset($this->_data[$key]) ? $this->_data[$key] : NULL; 80 | } 81 | 82 | /** 83 | * Set data all at once, by key, or by nested key. 84 | * 85 | * Overridden to allow full arrays and nested values to be set. Otherwise, 86 | * adding a value to an array using setData would lose the proper order. 87 | * 88 | * @param string|array $key 89 | * @param mixed $value 90 | * @param mixed $_value 91 | * @return Cm_Mongo_Model_Abstract 92 | */ 93 | public function setData($key, $value = NULL, $_value = NULL) 94 | { 95 | // Set all new data 96 | if(is_array($key)) { 97 | if($this->_origData) { 98 | if($this->isObjectNew() === FALSE) { 99 | foreach($this->_data as $k => $v) { 100 | $this->op('$unset', $k, 1); 101 | } 102 | } 103 | $this->_data = array(); 104 | foreach($key as $k => $v) { 105 | $this->setData($k, $v); 106 | } 107 | } 108 | else { 109 | $this->_hasDataChanges = true; 110 | $this->_data = $key; 111 | } 112 | } 113 | 114 | // Set one key to a value 115 | else if($_value === NULL) { 116 | if( ! isset($this->_data[$key]) || $this->_data[$key] !== $value) { 117 | $this->_hasDataChanges = true; 118 | $this->_data[$key] = $value; 119 | 120 | // Set after unset overrides the unset for this field including any child fields 121 | if(isset($this->_operations['$unset'])) { 122 | if(isset($this->_operations['$unset'][$key])) { 123 | unset($this->_operations['$unset'][$key]); 124 | } 125 | foreach($this->_operations['$unset']->getArrayCopy() as $_key => $_value) { 126 | if(strpos($_key, "$key.") !== FALSE) { 127 | unset($this->_operations['$unset'][$_key]); 128 | } 129 | } 130 | } 131 | 132 | // Setting a new embedded collection replaces the old embedded collection 133 | if($value instanceof Cm_Mongo_Model_Resource_Collection_Embedded 134 | && is_array($this->_origData) && array_key_exists($key, $this->_origData) 135 | && $this->_origData[$key] instanceof Cm_Mongo_Model_Resource_Collection_Embedded 136 | && $this->_origData[$key] != $value 137 | ) { 138 | $this->_origData[$key]->isReplaced(TRUE); 139 | } 140 | } 141 | } 142 | 143 | // Set a nested key to a value 144 | else { 145 | if( ! isset($this->_data[$key])) { 146 | $this->_data[$key] = array(); 147 | } 148 | $this->_hasDataChanges = true; 149 | $this->_data[$key][$value] = $_value; 150 | } 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * Unsets the data at the given key. 157 | * 158 | * Overridden to add an $unset operation for when the object is saved. 159 | * 160 | * @param string $key 161 | * @return Cm_Mongo_Model_Abstract 162 | */ 163 | public function unsetData($key=null) 164 | { 165 | if( ! is_null($key) && $this->isObjectNew() === FALSE && ! isset($this->getResource()->getFieldMappings()->$key->required)) { 166 | $this->op('$unset', $key, 1); 167 | } 168 | return parent::unsetData($key); 169 | } 170 | 171 | /** 172 | * Loads data into the model using the resource hydration method. 173 | * 174 | * @param array $data 175 | * @return Cm_Mongo_Model_Abstract 176 | */ 177 | public function loadData($data) 178 | { 179 | $this->getResource()->hydrate($this, (array) $data, FALSE); 180 | return $this; 181 | } 182 | 183 | /** 184 | * Retrieve an embedded object using the schema info by key 185 | * 186 | * @param string $field 187 | * @return Cm_Mongo_Model_Abstract 188 | */ 189 | protected function _getEmbeddedObject($field) 190 | { 191 | if( ! $this->hasData($field) || $this->getData($field) === NULL) { 192 | $object = Mage::getModel($this->getResource()->getFieldModelName($field)); /* @var $object Cm_Mongo_Model_Abstract */ 193 | $this->_setEmbeddedObject($field, $object); 194 | } 195 | else if( ! $this->getData($field) instanceof Cm_Mongo_Model_Abstract) { 196 | throw new Exception('Expected data to be an embedded object.'); 197 | } 198 | return $this->getData($field); 199 | } 200 | 201 | /** 202 | * Set an embedded object by key 203 | * 204 | * @param string $field 205 | * @param Cm_Mongo_Model_Abstract $object 206 | * @return Cm_Mongo_Model_Abstract 207 | */ 208 | protected function _setEmbeddedObject($field, Cm_Mongo_Model_Abstract $object) 209 | { 210 | $object->_setEmbeddedIn($this, $this->getFieldPath($field)); 211 | $this->setData($field, $object); 212 | if( ! isset($this->_children)) { 213 | $this->_children = new ArrayObject(); 214 | } 215 | $this->_children[$field] = $object; 216 | return $this; 217 | } 218 | 219 | /** 220 | * Retrieve an embedded collection using the schema info by key 221 | * 222 | * @param string $field 223 | * @return Cm_Mongo_Model_Resource_Collection_Embedded 224 | */ 225 | protected function _getEmbeddedCollection($field) 226 | { 227 | if( ! $this->hasData($field) || $this->getData($field) === NULL) { 228 | $collection = new Cm_Mongo_Model_Resource_Collection_Embedded; 229 | $collection->setItemObjectClass($this->getResource()->getFieldModelName($field)); 230 | $this->_setEmbeddedCollection($field, $collection); 231 | } 232 | else if( ! $this->getData($field) instanceof Cm_Mongo_Model_Resource_Collection_Embedded) { 233 | throw new Exception('Expected data to be an embedded collection.'); 234 | } 235 | return $this->getData($field); 236 | } 237 | 238 | /** 239 | * Set an embedded collection by key, used by embeddedSet type 240 | * 241 | * @param string $field 242 | * @param Cm_Mongo_Model_Resource_Collection_Embedded $collection 243 | * @return Cm_Mongo_Model_Abstract 244 | */ 245 | protected function _setEmbeddedCollection($field, Cm_Mongo_Model_Resource_Collection_Embedded $collection) 246 | { 247 | // Setup proper nesting for future items added 248 | $path = $this->getFieldPath($field); 249 | $collection->_setEmbeddedIn($this, $path); 250 | 251 | // Set proper nesting for existing items 252 | $i = 0; 253 | foreach($collection->getItems() as $item) { 254 | $item->_setEmbeddedIn($this, $path.'.'.$i); 255 | $i++; 256 | } 257 | 258 | // Set data and add collection to children for proper reset and hasDataChanges operation 259 | $this->setData($field, $collection); 260 | if( ! isset($this->_children)) { 261 | $this->_children = new ArrayObject(); 262 | } 263 | $this->_children[$field] = $collection; 264 | return $this; 265 | } 266 | 267 | /** 268 | * Set the parent object and the path relative to the parent. 269 | * 270 | * Although this method is public it should not be used except by the _setEmbeddedObject method. 271 | * 272 | * @param Cm_Mongo_Model_Abstract $parent 273 | * @param string $path 274 | * @return Cm_Mongo_Model_Abstract 275 | */ 276 | public function _setEmbeddedIn($parent, $path) 277 | { 278 | $this->_root = $parent->getRootObject(); 279 | $this->_path = $path.'.'; 280 | return $this; 281 | } 282 | 283 | /** 284 | * Unset an embedded object or collection 285 | * 286 | * @param string $field 287 | */ 288 | protected function _unsetEmbeddedObject($field) 289 | { 290 | if($object = $this->getData($field)) { 291 | unset($this->_children[$field]); 292 | $object->reset(); 293 | $this->unsetData($field); 294 | } 295 | } 296 | 297 | /** 298 | * Get the root object (highest parent, or self if this is the parent object) 299 | * 300 | * @return Cm_Mongo_Model_Abstract 301 | */ 302 | public function getRootObject() 303 | { 304 | return isset($this->_root) ? $this->_root : $this; 305 | } 306 | 307 | /** 308 | * Get the full path of the given field relative to the root object. 309 | * 310 | * @param string $field 311 | * @return string 312 | */ 313 | public function getFieldPath($field = '') 314 | { 315 | return (isset($this->_path) ? $this->_path : '').$field; 316 | } 317 | 318 | /** 319 | * Get the referenced object. Attempts to load the object on first retrieval. 320 | * 321 | * @param string $field 322 | * @return Cm_Mongo_Model_Abstract 323 | */ 324 | public function getReferencedObject($field) 325 | { 326 | if( ! isset($this->_references[$field])) 327 | { 328 | $modelName = $this->getResource()->getFieldModelName($field); 329 | 330 | // If there is a reference, load from cache or from database 331 | if($refId = $this->getData($field)) 332 | { 333 | $object = Mage::getResourceSingleton($modelName)->getCachedObject($refId); 334 | if( ! $object) { 335 | $object = Mage::getModel($modelName)->isCacheEnabled(TRUE)->load($refId); 336 | } 337 | } 338 | 339 | // Otherwise use empty object 340 | else 341 | { 342 | $object = Mage::getModel($modelName); 343 | } 344 | $this->_references[$field] = $object; 345 | } 346 | return $this->_references[$field]; 347 | } 348 | 349 | /** 350 | * Sets the referenced object. Stores an internal reference and sets the field to the object's ID. 351 | * 352 | * @param string $field 353 | * @param Cm_Mongo_Model_Abstract $object 354 | * @return Cm_Mongo_Model_Abstract 355 | */ 356 | public function setReferencedObject($field, Cm_Mongo_Model_Abstract $object) 357 | { 358 | $this->setData($field, $object->getId()); 359 | $this->_references[$field] = $object; 360 | return $this; 361 | } 362 | 363 | /** 364 | * Get a collection of referenced objects (supports referencedSet and referencedHash field types) 365 | * 366 | * @param string $field 367 | * @throws Exception 368 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 369 | */ 370 | public function getReferencedCollection($field) 371 | { 372 | if( ! isset($this->_references[$field])) { 373 | $mapping = $this->getResource()->getFieldMapping($field); 374 | switch ((string) $mapping->type) 375 | { 376 | case 'referenceSet': 377 | $ids = $this->getData($field); 378 | break; 379 | case 'referenceHash': 380 | $ids = array(); 381 | if ($this->getData($field)) { 382 | $idField = (string) $mapping->id_field; 383 | if ( ! $idField) { 384 | throw new Exception("Field definition for $field is missing 'id_field' definition."); 385 | } 386 | foreach ($this->getData($field) as $item) { 387 | if ( ! empty($item[$idField])) { 388 | $ids[] = $item[$idField]; 389 | } 390 | } 391 | } 392 | break; 393 | default: 394 | throw new Exception('Field type does not support referenced collections.'); 395 | } 396 | $modelName = $this->getResource()->getFieldModelName($field); 397 | $collection = Mage::getSingleton($modelName)->getCollection()->addFieldToFilter('_id', 'in', $ids); 398 | $this->_references[$field] = $collection; 399 | } 400 | return $this->_references[$field]; 401 | } 402 | 403 | /** 404 | * Queue an operation to be performed during the next save. 405 | * 406 | * @see http://www.mongodb.org/display/DOCS/Updating 407 | * 408 | * If $key is an array, the operation will be applied to each key => value pair. 409 | * 410 | * @param string|Array $op 411 | * @param string|Array $key 412 | * @param mixed $value 413 | * @return Cm_Mongo_Model_Abstract 414 | */ 415 | public function op($op, $key = NULL, $value = NULL) 416 | { 417 | // Allow multiple operations if $op is an array or $key is an array 418 | if(is_array($op)) { 419 | foreach($op as $k => $v) { 420 | $this->op($k, $v); 421 | } 422 | return $this; 423 | } 424 | if(is_array($key)) { 425 | foreach($key as $k => $v) { 426 | $this->op($op, $k, $v); 427 | } 428 | return $this; 429 | } 430 | 431 | // All operations are routed to the root object 432 | if(isset($this->_root)) { 433 | $this->getRootObject()->op($op, $this->_path.$key, $value); 434 | return $this; 435 | } 436 | 437 | // Save operation for later 438 | if( ! isset($this->_operations)) { 439 | $this->_operations = new ArrayObject(); 440 | } 441 | if( ! isset($this->_operations[$op])) { 442 | $this->_operations[$op] = new ArrayObject(); 443 | } 444 | 445 | // Unset the parent overwrites the children 446 | if($op == '$unset') { 447 | if(isset($this->_operations['$unset'])) { 448 | foreach($this->_operations['$unset']->getArrayCopy() as $_key => $_value) { 449 | if(strpos($_key, "$key.") !== FALSE) { 450 | unset($this->_operations['$unset'][$_key]); 451 | } 452 | } 453 | } 454 | $this->_operations[$op][$key] = 1; 455 | } 456 | // Combine multiple $push/$pull into $pushAll/$pullAll 457 | else if( 458 | in_array($op, array('$push','$pull')) && 459 | ( 460 | isset($this->_operations[$op][$key]) || 461 | (isset($this->_operations["{$op}All"]) && isset($this->_operations["{$op}All"][$key])) 462 | ) 463 | ) { 464 | if( ! isset($this->_operations["{$op}All"])) { 465 | $this->_operations["{$op}All"] = array(); 466 | } 467 | if( ! isset($this->_operations["{$op}All"][$key])) { 468 | $this->_operations["{$op}All"][$key] = array(); 469 | } 470 | if( isset($this->_operations[$op][$key]) ) { 471 | $this->_operations["{$op}All"][$key][] = $this->_operations[$op][$key]; 472 | unset($this->_operations[$op][$key]); 473 | if( ! $this->_operations[$op]) { 474 | unset($this->_operations[$op]); 475 | } 476 | } 477 | $this->_operations["{$op}All"][$key][] = $value; 478 | } 479 | // $pushAll/$pullAll gets aggregated instead of overwritten 480 | else if(in_array($op, array('$pushAll','$pullAll'))) { 481 | if( ! isset($this->_operations[$op][$key])) { 482 | $this->_operations[$op][$key] = array(); 483 | } 484 | $this->_operations[$op][$key][] = $value; 485 | } 486 | // All others overwrite 487 | else { 488 | $this->_operations[$op][$key] = $value; 489 | } 490 | 491 | $this->_hasDataChanges = TRUE; 492 | return $this; 493 | } 494 | 495 | /** 496 | * Retrieve all pending operations. Only needs to be used by resource model. 497 | * 498 | * @return array 499 | */ 500 | public function getPendingOperations() 501 | { 502 | return isset($this->_operations) ? $this->_operations->getArrayCopy() : array(); 503 | } 504 | 505 | /** 506 | * Reset the pending operations. Only needs to be used by resource model. 507 | * 508 | * @return Cm_Mongo_Model_Abstract 509 | */ 510 | public function resetPendingOperations() 511 | { 512 | $this->_operations = NULL; 513 | return $this; 514 | } 515 | 516 | /** 517 | * Set additional criteria that will be added to the update query on the next save. 518 | * The criteria will be cleared after each save. 519 | * 520 | * @param array|null $query 521 | * @return Cm_Mongo_Model_Abstract 522 | */ 523 | public function setAdditionalSaveCriteria(array $query = NULL) 524 | { 525 | $this->_additionalSaveCriteria = $query; 526 | return $this; 527 | } 528 | 529 | /** 530 | * Merge more save criteria with the existing save criteria 531 | * 532 | * @param array|null $query 533 | * @return \Cm_Mongo_Model_Abstract 534 | */ 535 | public function addAdditionalSaveCriteria(array $query = NULL) 536 | { 537 | $this->_additionalSaveCriteria = array_merge((array)$this->_additionalSaveCriteria, $query); 538 | return $this; 539 | } 540 | 541 | /** 542 | * @return array|null 543 | */ 544 | public function getAdditionalSaveCriteria() 545 | { 546 | return $this->_additionalSaveCriteria; 547 | } 548 | 549 | /** 550 | * Set options that override the defaults when the object is saved. 551 | * 552 | * @param array|null $options 553 | * @return Cm_Mongo_Model_Abstract 554 | */ 555 | public function setAdditionalSaveOptions(array $options = NULL) 556 | { 557 | $this->_additionalSaveOptions = $options; 558 | return $this; 559 | } 560 | 561 | /** 562 | * @return array|null 563 | */ 564 | public function getAdditionalSaveOptions() 565 | { 566 | return $this->_additionalSaveOptions; 567 | } 568 | 569 | /** 570 | * @param null|bool $status 571 | * @return Cm_Mongo_Model_Abstract 572 | */ 573 | public function setLastUpdateStatus($status) 574 | { 575 | $this->_lastUpdateStatus = $status; 576 | return $this; 577 | } 578 | 579 | /** 580 | * @return null|bool 581 | */ 582 | public function getLastUpdateStatus() 583 | { 584 | return $this->_lastUpdateStatus; 585 | } 586 | 587 | /** 588 | * Overriden to check all embedded objects for data changes as well. 589 | * 590 | * @return boolean 591 | */ 592 | public function hasDataChanges() 593 | { 594 | if($this->_hasDataChanges) { 595 | return TRUE; 596 | } 597 | 598 | if(isset($this->_children)) { 599 | foreach($this->_children as $value) { 600 | if($value->hasDataChanges()) { 601 | return TRUE; 602 | } 603 | } 604 | } 605 | return FALSE; 606 | } 607 | 608 | /** 609 | * Initialize object original data 610 | * 611 | * Overridden so that when called without arguments it resets the state of hasDataChanges and calls 612 | * setOrigData on the embedded objects 613 | * 614 | * @param string $key 615 | * @param mixed $data 616 | * @return Varien_Object 617 | */ 618 | public function setOrigData($key=null, $data=null) 619 | { 620 | if (is_null($key)) { 621 | $this->_origData = $this->_data; 622 | if($this->_children) { 623 | foreach($this->_children as $object) { 624 | if($object instanceof Cm_Mongo_Model_Abstract) { 625 | $object->setOrigData(); 626 | } else if($object instanceof Cm_Mongo_Model_Resource_Collection_Embedded) { 627 | $object->walk('setOrigData'); 628 | } 629 | } 630 | } 631 | $this->_hasDataChanges = FALSE; 632 | } else { 633 | $this->_origData[$key] = $data; 634 | } 635 | return $this; 636 | } 637 | 638 | /** 639 | * When memory usage is important, use this to ensure there are no memory leaks. 640 | * 641 | * Uses _clearReferences and _clearData to be nearly synonymous with new clearInstance() method of Magento 1.6 642 | * and may therefore eventually be deprecated. 643 | * 644 | * @return Cm_Mongo_Model_Abstract 645 | */ 646 | public function reset() 647 | { 648 | $this->_clearReferences(); 649 | $this->_clearData(); 650 | return $this; 651 | } 652 | 653 | /** 654 | * Get the specified field as a Zend_Date or formatted if $format is given. 655 | * 656 | * @param string $field 657 | * @param string $format 658 | * @param boolean $useTimezone 659 | * @return Zend_Date|string 660 | */ 661 | public function getAsZendDate($field, $format = null, $useTimezone = true) 662 | { 663 | $data = $this->getData($field); 664 | if( ! $data) { 665 | return NULL; 666 | } 667 | 668 | // Detect any string stored as date as datestring 669 | $type = $this->getResource()->getFieldType($field); 670 | if ($type == 'string' && strlen($data) == 10 && preg_match('/^\d{4}-\d{2}-\d{2}$/', $data)) { 671 | $type = 'datestring'; 672 | } 673 | 674 | switch($type) 675 | { 676 | case 'MongoDate': 677 | $date = Mage::app()->getLocale()->date($data->sec, null, null, $useTimezone); 678 | break; 679 | 680 | case 'datestring': 681 | $date = Mage::app()->getLocale()->date($data, Varien_Date::DATE_INTERNAL_FORMAT, null, false); 682 | break; 683 | 684 | case 'timestamp': 685 | case 'int': 686 | case 'float': 687 | default: 688 | $date = Mage::app()->getLocale()->date($data, null, null, $useTimezone); 689 | break; 690 | } 691 | if($format !== null) { 692 | return $date->toString($format); 693 | } 694 | return $date; 695 | } 696 | 697 | /** 698 | * Overridden to save the object in the cache if allowed 699 | * 700 | * @return Cm_Mongo_Model_Abstract 701 | */ 702 | protected function _afterLoad() 703 | { 704 | if($this->isCacheEnabled()) { 705 | $this->getResource()->addObjectToCache($this); 706 | } 707 | return parent::_afterLoad(); 708 | } 709 | 710 | /** 711 | * Overridden to save the object in the cache if allowed 712 | * 713 | * @return Cm_Mongo_Model_Abstract 714 | */ 715 | protected function _afterDelete() 716 | { 717 | if($this->isCacheEnabled()) { 718 | $this->getResource()->removeObjectFromCache($this); 719 | } 720 | return parent::_afterDelete(); 721 | } 722 | 723 | /** 724 | * Overridden to not set an object as new when there is no id to support upserts. 725 | * 726 | * @return Cm_Mongo_Model_Abstract 727 | */ 728 | protected function _beforeSave() 729 | { 730 | Mage::dispatchEvent('model_save_before', array('object'=>$this)); 731 | Mage::dispatchEvent($this->_eventPrefix.'_save_before', $this->_getEventData()); 732 | return $this; 733 | } 734 | 735 | /** 736 | * Overridden to reset original data after the _afterSave callback. 737 | * 738 | * @return Cm_Mongo_Model_Abstract 739 | */ 740 | public function afterCommitCallback() 741 | { 742 | parent::afterCommitCallback(); 743 | if($this->_dataSaveAllowed) { 744 | $this->setOrigData(); 745 | } 746 | return $this; 747 | } 748 | 749 | /** 750 | * Enable/disable the cache or get the current state 751 | * 752 | * @param boolean $value 753 | * @return boolean|Cm_Mongo_Model_Abstract 754 | */ 755 | public function isCacheEnabled($value = NULL) 756 | { 757 | if($value !== NULL) { 758 | $this->_isCacheEnabled = $value; 759 | return $this; 760 | } 761 | return $this->_isCacheEnabled; 762 | } 763 | 764 | /** 765 | * Made final so that the calling parent method is not necessary and therefore cannot be forgotten. 766 | * Use __clearReferences instead. 767 | */ 768 | final protected function _clearReferences() 769 | { 770 | $this->__clearReferences(); 771 | $this->_references = array(); 772 | unset($this->_root); 773 | if(isset($this->_children)) { 774 | foreach($this->_children as $object) { 775 | $object->reset(); 776 | } 777 | unset($this->_children); 778 | } 779 | } 780 | 781 | /** 782 | * Made final so that the calling parent method is not necessary and therefore cannot be forgotten. 783 | * Use __clearData instead. 784 | */ 785 | final protected function _clearData() 786 | { 787 | $this->__clearData(); 788 | $this->setData(array()); 789 | $this->setOrigData(); 790 | $this->resetPendingOperations(); 791 | $this->_hasDataChanges = FALSE; 792 | $this->_isObjectNew = TRUE; 793 | } 794 | 795 | /** 796 | * Use this callback to prevent memory leaks on reset (destroy any possible circular references) 797 | */ 798 | protected function __clearReferences() { } 799 | 800 | /** 801 | * Use this callback to prevent memory leaks on reset (destroy any data that could have circular references) 802 | */ 803 | protected function __clearData() { } 804 | 805 | } 806 | -------------------------------------------------------------------------------- /code/Model/Entity.php: -------------------------------------------------------------------------------- 1 | _entityIdField)) { 29 | $this->_entityIdField = $this->getEntityType()->getEntityIdField(); 30 | if (empty($this->_entityIdField)) { 31 | $this->_entityIdField = self::DEFAULT_ENTITY_ID_FIELD; 32 | } 33 | } 34 | return $this->_entityIdField; 35 | } 36 | 37 | /** 38 | * Check whether the attribute is a real field in entity table 39 | * 40 | * @see Mage_Eav_Model_Entity_Abstract::getAttribute for $attribute format 41 | * @param integer|string|Mage_Eav_Model_Entity_Attribute_Abstract $attribute 42 | * @return unknown 43 | */ 44 | public function isAttributeStatic($attribute) 45 | { 46 | return TRUE; 47 | } 48 | 49 | /** 50 | * Enter description here... 51 | * 52 | * @param Mage_Eav_Model_Entity_Attribute_Abstract $attribute 53 | * @param Varien_Object $object 54 | * @return boolean 55 | */ 56 | public function checkAttributeUniqueValue(Mage_Eav_Model_Entity_Attribute_Abstract $attribute, $object) 57 | { 58 | $value = $object->getData($attribute->getAttributeCode()); 59 | if ($attribute->getBackend()->getType() == 'datetime'){ 60 | $date = new Zend_Date($value); 61 | $value = $date->toString(Varien_Date::DATETIME_INTERNAL_FORMAT); 62 | } 63 | 64 | $data = $this->_getWriteAdapter() 65 | ->getCollection($this->getEntityTable()) 66 | ->findOne(array( 67 | $attribute->getAttributeCode() => $value 68 | ), array($this->getEntityIdField() => 1)); 69 | 70 | if ($object->getId()) { 71 | if ($data) { 72 | return $data[$this->getEntityIdField()] == $object->getId(); 73 | } 74 | return true; 75 | } 76 | else { 77 | return ! $data; 78 | } 79 | } 80 | 81 | /** 82 | * Load entity's attributes into the object 83 | * 84 | * @param Varien_Object $object 85 | * @param integer $entityId 86 | * @param array|null $attributes 87 | * @return Mage_Eav_Model_Entity_Abstract 88 | */ 89 | public function load($object, $entityId, $attributes=array()) 90 | { 91 | /** 92 | * Load object base row data 93 | */ 94 | if (empty($attributes)) { 95 | $this->loadAllAttributes($object); 96 | $attributes = array_keys($this->_attributesByCode); 97 | } else { 98 | foreach ($attributes as $attrCode) { 99 | $this->getAttribute($attrCode); 100 | } 101 | } 102 | 103 | array_fill_keys($attributes, 1); 104 | 105 | $row = $this->_getReadAdapter() 106 | ->getCollection($this->getEntityTable()) 107 | ->findOne(array( 108 | $this->getEntityIdField() => $entityId 109 | ), $attributes); 110 | 111 | if (is_array($row)) { 112 | $object->addData($row); 113 | $object->_isNewObject(FALSE); 114 | } 115 | 116 | $object->setOrigData(); 117 | Varien_Profiler::start('__MONGO_EAV_LOAD_MODEL_AFTER_LOAD__'); 118 | $this->_afterLoad($object); 119 | Varien_Profiler::stop('__MONGO_EAV_LOAD_MODEL_AFTER_LOAD__'); 120 | 121 | return $this; 122 | } 123 | 124 | /** 125 | * Save entity's attributes into the object's resource 126 | * 127 | * @param Varien_Object $object 128 | * @return Mage_Eav_Model_Entity_Abstract 129 | */ 130 | public function save(Varien_Object $object) 131 | { 132 | if ($object->isDeleted()) { 133 | return $this->delete($object); 134 | } 135 | 136 | if (!$this->isPartialSave()) { 137 | $this->loadAllAttributes($object); 138 | } 139 | 140 | if (!$object->getEntityTypeId()) { 141 | $object->setEntityTypeId($this->getTypeId()); 142 | } 143 | 144 | $this->_beforeSave($object); 145 | $this->_processSaveData($this->_collectSaveData($object)); 146 | $this->_afterSave($object); 147 | 148 | return $this; 149 | } 150 | 151 | /** 152 | * Prepare entity object data for save 153 | * 154 | * result array structure: 155 | * array ( 156 | * 'newObject', 'entityRow', 'insert', 'update', 'delete' 157 | * ) 158 | * 159 | * @param Varien_Object $newObject 160 | * @return array 161 | */ 162 | protected function _collectSaveData($newObject) 163 | { 164 | $newData = $newObject->getData(); 165 | $entityId = $newObject->getData($this->getEntityIdField()); 166 | 167 | // define result data 168 | $entityRow = array(); 169 | $insert = array(); 170 | $update = array(); 171 | $delete = array(); 172 | 173 | if (!empty($entityId)) { 174 | $origData = $newObject->getOrigData(); 175 | /** 176 | * get current data in db for this entity if original data is empty 177 | */ 178 | if (empty($origData) && $newObject->isNewObject() === FALSE) { 179 | $origData = $this->_getOrigObject($newObject)->getOrigData(); 180 | } 181 | 182 | /** 183 | * drop attributes that are unknown in new data 184 | * not needed after introduction of partial entity loading 185 | */ 186 | if($origData) { 187 | foreach ($origData as $k => $v) { 188 | if (!array_key_exists($k, $newData)) { 189 | unset($origData[$k]); 190 | } 191 | } 192 | } 193 | } else { 194 | $origData = array(); 195 | } 196 | 197 | $attributeCodes = array_keys($this->_attributesByCode); 198 | 199 | foreach ($newData as $k => $v) { 200 | /** 201 | * Check attribute information 202 | */ 203 | if (is_numeric($k) || is_array($v)) { 204 | continue; 205 | } 206 | /** 207 | * Check if data key is presented in static fields or attribute codes 208 | */ 209 | if (!in_array($k, $attributeCodes)) { 210 | continue; 211 | } 212 | 213 | $attribute = $this->getAttribute($k); 214 | if (empty($attribute)) { 215 | continue; 216 | } 217 | 218 | $attrCode = $attribute->getAttributeCode(); 219 | 220 | /** 221 | * Check comparability for attribute value 222 | */ 223 | if (array_key_exists($k, $origData)) { 224 | if ($this->_isAttributeValueEmpty($attribute, $v)) { 225 | $delete[$attrCode] = 1; 226 | } else if ($v !== $origData[$k]) { 227 | $update[$attrCode] = $v; 228 | } 229 | } else if (!$this->_isAttributeValueEmpty($attribute, $v)) { 230 | $insert[$attrCode] = $v; 231 | } 232 | } 233 | 234 | $result = compact('newObject', 'insert', 'update', 'delete'); 235 | return $result; 236 | } 237 | 238 | /** 239 | * Save object collected data 240 | * 241 | * @param array $saveData array('newObject', 'insert', 'update', 'delete') 242 | * @return Mage_Eav_Model_Entity_Abstract 243 | */ 244 | protected function _processSaveData($saveData) 245 | { 246 | $object = NULL; // ? 247 | $newObject = $saveData['new_Object']; 248 | $insert = $saveData['insert']; 249 | $update = $saveData['update']; 250 | $delete = $saveData['delete']; 251 | $entityIdField = $this->getEntityIdField(); 252 | $entityId = $newObject->getId(); 253 | $condition = array($entityIdField => $entityId); 254 | 255 | // Upsert when status is unknown 256 | if ($object->isNewObject() === NULL) { 257 | $this->_getWriteAdapter()->getCollection($this->getEntityTable()) 258 | ->update($condition, $insert+$update, array('upsert' => TRUE, 'multiple' => FALSE, 'safe' => TRUE)); 259 | } 260 | 261 | // Insert new objects 262 | else if($object->isNewObject()) { 263 | if($update) { 264 | $this->_getWriteAdapter()->getCollection($this->getEntityTable()) 265 | ->update($insert, $update, array('upsert' => TRUE, 'multiple' => FALSE, 'safe' => TRUE)); 266 | } 267 | else { 268 | $this->_getWriteAdapter()->getCollection($this->getEntityTable()) 269 | ->insert($insert, array('safe' => TRUE)); 270 | } 271 | } 272 | 273 | // Update existing objects 274 | else { 275 | $this->_getWriteAdapter()->getCollection($this->getEntityTable()) 276 | ->update($condition, $insert+$update, array('multiple' => FALSE, 'safe' => TRUE)); 277 | } 278 | 279 | /** 280 | * insert attribute values 281 | */ 282 | if (!empty($insert)) { 283 | foreach ($insert as $attrCode => $value) { 284 | $attribute = $this->getAttribute($attrCode); 285 | $this->_insertAttribute($newObject, $attribute, $value); 286 | } 287 | } 288 | 289 | /** 290 | * update attribute values 291 | */ 292 | if (!empty($update)) { 293 | foreach ($update as $attrCode => $value) { 294 | $attribute = $this->getAttribute($attrCode); 295 | //$this->_updateAttribute($newObject, $attribute, $value); 296 | } 297 | } 298 | 299 | /** 300 | * delete empty attribute values 301 | */ 302 | if (!empty($delete)) { 303 | foreach ($delete as $attrCode => $value) { 304 | //$this->_deleteAttributes($newObject, $attrCode); 305 | } 306 | } 307 | 308 | $this->_processAttributeValues(); 309 | 310 | return $this; 311 | } 312 | 313 | /** 314 | * Set entity attribute value 315 | * 316 | * Collect for mass save 317 | * 318 | * @param Mage_Core_Model_Abstract $object 319 | * @param Mage_Eav_Model_Entity_Attribute_Abstract $attribute 320 | * @param mixed $value 321 | * @return Mage_Eav_Model_Entity_Abstract 322 | */ 323 | protected function _setAttribute($object, $attribute, $value) 324 | { 325 | $this->_attributeValuesToSave['$set'][$attribute->getAttributeCode()] = $this->_prepareValueForSave($value, $attribute); 326 | 327 | return $this; 328 | } 329 | 330 | /** 331 | * Prepare value for save 332 | * 333 | * @param mixed $value 334 | * @param Mage_Eav_Model_Entity_Attribute_Abstract $attribute 335 | * @return mixed 336 | */ 337 | protected function _prepareValueForSave($value, Mage_Eav_Model_Entity_Attribute_Abstract $attribute) 338 | { 339 | if ($attribute->getBackendType() == 'decimal') { 340 | return Mage::app()->getLocale()->getNumber($value); 341 | } 342 | return $value; 343 | } 344 | 345 | /** 346 | * Save attribute 347 | * 348 | * @param Varien_Object $object 349 | * @param string $attributeCode 350 | * @return Mage_Eav_Model_Entity_Abstract 351 | */ 352 | public function saveAttribute(Varien_Object $object, $attributeCode) 353 | { 354 | $attribute = $this->getAttribute($attributeCode); 355 | $backend = $attribute->getBackend(); 356 | $entity = $attribute->getEntity(); 357 | $entityIdField = $entity->getEntityIdField(); 358 | 359 | $newValue = $object->getData($attributeCode); 360 | if ($attribute->isValueEmpty($newValue)) { 361 | $newValue = null; 362 | } 363 | 364 | $condition = array($object->getIdFieldName() => $object->getId()); 365 | 366 | if ( ! is_null($newValue)) { 367 | $operation = array('$set' => array($attributeCode => $newValue)); 368 | } else { 369 | $operation = array('$unset' => array($attributeCode => 1)); 370 | } 371 | $this->_getWriteAdapter()->getCollection($this->getEntityTable()) 372 | ->update($condition, $operation, array('multiple' => FALSE, 'safe' => TRUE)); 373 | 374 | return $this; 375 | } 376 | 377 | /** 378 | * Delete entity using current object's data 379 | * 380 | * @param $object 381 | * @return Mage_Eav_Model_Entity_Abstract 382 | */ 383 | public function delete($object) 384 | { 385 | if (is_numeric($object)) { 386 | $id = (int)$object; 387 | } elseif ($object instanceof Varien_Object) { 388 | $id = (int)$object->getId(); 389 | } 390 | 391 | $this->_beforeDelete($object); 392 | 393 | $this->_getWriteAdapter()->getCollection($this->getEntityTable())->delete(array($this->getEntityIdField() => $id)); 394 | 395 | $this->_afterDelete($object); 396 | return $this; 397 | } 398 | 399 | } 400 | -------------------------------------------------------------------------------- /code/Model/Enum.php: -------------------------------------------------------------------------------- 1 | value pairs. 4 | */ 5 | class Cm_Mongo_Model_Enum 6 | extends Cm_Mongo_Model_Abstract 7 | implements Mage_Eav_Model_Entity_Attribute_Source_Interface 8 | { 9 | 10 | protected function _construct() 11 | { 12 | $this->_init('mongo/enum'); 13 | } 14 | 15 | /** 16 | * @param string $key 17 | * @param array $data 18 | * @return Cm_Mongo_Model_Enum 19 | */ 20 | public function addDefaultValue($key, $data) 21 | { 22 | $defaults = $this->getDefaults(); 23 | $defaults[$key] = $data; 24 | $this->setDefaults($defaults); 25 | return $this; 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function getValues() 32 | { 33 | if($this->getStoreId()) { 34 | $storeCode = Mage::app()->getStore($this->getStoreId())->getCode(); 35 | $stores = $this->getStores(); 36 | if($stores && isset($stores[$storeCode])) { 37 | return (array) $stores[$storeCode]; 38 | } 39 | } 40 | return (array) $this->getDefaults(); 41 | } 42 | 43 | /** 44 | * @return int 45 | */ 46 | public function getValuesCount() 47 | { 48 | return count($this->getValues()); 49 | } 50 | 51 | /** 52 | * @param string $first 53 | * @return array 54 | */ 55 | public function toOptionArray($first = NULL) 56 | { 57 | $options = $this->getAllOptions(); 58 | if($first !== NULL) { 59 | array_unshift($options, array('value' => '', 'label' => $first)); 60 | } 61 | return $options; 62 | } 63 | 64 | /** 65 | * @param string $first 66 | * @return array 67 | */ 68 | public function toOptionHash($first = NULL) 69 | { 70 | $options = array(); 71 | if($first !== NULL) { 72 | $first[''] = $first; 73 | } 74 | $values = $this->getValues(); 75 | asort($values); 76 | foreach($values as $key => $data) { 77 | $options[$key] = $data['label']; 78 | } 79 | return $options; 80 | } 81 | 82 | /* 83 | * Implement methods for Mage_Eav_Model_Entity_Attribute_Source_Interface 84 | */ 85 | 86 | /** 87 | * @return array 88 | */ 89 | public function getAllOptions() 90 | { 91 | $options = array(); 92 | $values = $this->getValues(); 93 | asort($values); 94 | foreach($values as $key => $data) { 95 | $options[] = array('value' => $key, 'label' => $data['label']); 96 | } 97 | return $options; 98 | } 99 | 100 | /** 101 | * @param string $value 102 | * @return bool|string 103 | */ 104 | public function getOptionText($value) { 105 | $values = $this->getValues(); 106 | if( ! isset($values[$value])) { 107 | return FALSE; 108 | } 109 | return $values[$value]['label']; 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /code/Model/Fixture.php: -------------------------------------------------------------------------------- 1 | _mongoResource = Mage::getResourceModel('mongo/fixture'); 20 | } 21 | 22 | /** 23 | * @return \Cm_Mongo_Model_Mongo_Fixture 24 | */ 25 | protected function getMongoResource() 26 | { 27 | return $this->_mongoResource; 28 | } 29 | 30 | /** 31 | * Applies collection data into test database 32 | * 33 | * @param array $collections 34 | * @return Cm_Mongo_Model_Fixture 35 | */ 36 | protected function _applyCollections($collections) 37 | { 38 | if (!is_array($collections)) { 39 | throw new InvalidArgumentException( 40 | 'Collections part should be an associative list with keys as collection entity and values as list of documents' 41 | ); 42 | } 43 | 44 | foreach ($collections as $collectionEntity => $data) { 45 | $this->getMongoResource()->cleanCollection($collectionEntity); 46 | $this->_loadCollectionData($collectionEntity, $data); 47 | } 48 | } 49 | 50 | /** 51 | * Removes collection data from test data base 52 | * 53 | * @param array $collections 54 | * @return Cm_Mongo_Model_Fixture 55 | */ 56 | protected function _discardCollections($collections) 57 | { 58 | if (!is_array($collections)) { 59 | throw new InvalidArgumentException( 60 | 'Collections part should be an associative list with keys as collection entity and values as list of documents' 61 | ); 62 | } 63 | 64 | foreach ($collections as $collectionEntity => $data) { 65 | $this->getMongoResource()->cleanCollection($collectionEntity); 66 | } 67 | } 68 | 69 | /** 70 | * @param string $collectionEntity 71 | * @param array $data 72 | */ 73 | protected function _loadCollectionData($collectionEntity, $data) 74 | { 75 | if ( ! empty($data)) { 76 | $this->getMongoResource()->loadCollectionData($collectionEntity, $data); 77 | } 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /code/Model/Indexer.php: -------------------------------------------------------------------------------- 1 | _init('mongo/indexer'); 19 | } 20 | 21 | /** 22 | * Get Indexer name 23 | * 24 | * @return string 25 | */ 26 | public function getName() 27 | { 28 | return Mage::helper('mongo')->__('General Indexer'); 29 | } 30 | 31 | /** 32 | * Get Indexer description 33 | * 34 | * @return string 35 | */ 36 | public function getDescription() 37 | { 38 | return Mage::helper('mongo')->__('Basic general-purpose indexer for Mongo models.'); 39 | } 40 | 41 | /** 42 | * @return Cm_Mongo_Model_Indexer_Schema 43 | */ 44 | public function getSchema() 45 | { 46 | return Mage::getSingleton('mongo/indexer_schema'); 47 | } 48 | 49 | /** 50 | * Register indexer required data inside event object 51 | * 52 | * @param Mage_Index_Model_Event $event 53 | * @return void 54 | */ 55 | protected function _registerEvent(Mage_Index_Model_Event $event) 56 | { 57 | $event->addNewData('indexers', $this->getEntityIndexers($event)); 58 | } 59 | 60 | /** 61 | * Process event based on event state data 62 | * 63 | * @param Mage_Index_Model_Event $event 64 | */ 65 | protected function _processEvent(Mage_Index_Model_Event $event) 66 | { 67 | $data = $event->getNewData('indexers'); 68 | $object = $event->getDataObject(); /** @var $object Mage_Core_Model_Abstract */ 69 | 70 | foreach ($data['indexers'] as $indexer) { 71 | try { 72 | switch ($event->getType()) { 73 | case Mage_Index_Model_Event::TYPE_SAVE : 74 | $this->_getResource()->updateIndex($indexer, $object); 75 | break; 76 | case Mage_Index_Model_Event::TYPE_DELETE : 77 | $this->_getResource()->removeIndex($indexer, $object->getId()); 78 | break; 79 | case Mage_Index_Model_Event::TYPE_MASS_ACTION : 80 | case Mage_Index_Model_Event::TYPE_REINDEX : 81 | $indexerIds = (array) $object->getIndexerIds(); 82 | if (!empty($indexerIds)) { 83 | $actionType = ($object->getActionType() == self::ACTION_TYPE_REMOVE) 84 | ? self::ACTION_TYPE_REMOVE 85 | : self::ACTION_TYPE_UPDATE; 86 | switch ($actionType) { 87 | case self::ACTION_TYPE_UPDATE : 88 | $collection = $object->getCollection(); /** @var $collection Cm_Mongo_Model_Resource_Collection_Abstract */ 89 | $collection->addFieldToFilter('_id', 'in', $indexerIds); 90 | foreach ($collection as $item) { 91 | $this->_getResource()->updateIndex($indexer, $item); 92 | } 93 | break; 94 | case self::ACTION_TYPE_REMOVE : 95 | $this->_getResource()->removeIndex($indexer, $indexerIds); 96 | break; 97 | } 98 | } 99 | break; 100 | } 101 | } catch (Exception $e) { 102 | Mage::logException($e); 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Check if indexer matched specific entity and action type 109 | * 110 | * @param string $entity 111 | * @param string $type 112 | * @return bool 113 | */ 114 | public function matchEntityAndType($entity, $type) 115 | { 116 | return (bool) count($this->getSchema()->getEntityIndexers($entity)); 117 | } 118 | 119 | /** 120 | * Get indexers for the entity 121 | * 122 | * @param Mage_Index_Model_Event $event 123 | * @return array 124 | */ 125 | public function getEntityIndexers(Mage_Index_Model_Event $event) 126 | { 127 | $indexers = array(); 128 | $allIndexers = array(); 129 | $object = $event->getDataObject(); /** @var $object Mage_Core_Model_Abstract */ 130 | $duplicates = array(); 131 | foreach ($this->getSchema()->getEntityIndexers($event->getEntity()) as $indexer) { 132 | if (!$this->_validate($indexer)) { 133 | continue; 134 | } 135 | if (!isset($indexer->duplicates)) { 136 | $allIndexers[$indexer->fieldName.'/'.$indexer->indexerName] = $indexer; 137 | } 138 | if ($object->dataHasChangedFor($indexer->fieldName) || $object->isDeleted()) { 139 | if (isset($indexer->duplicates)) { 140 | $duplicates[$indexer->duplicates] = $indexer->method; 141 | } else { 142 | $indexers[$indexer->fieldName.'/'.$indexer->indexerName] = $indexer; 143 | } 144 | } 145 | } 146 | foreach ($duplicates as $key => $method) { 147 | if (isset($allIndexers[$key])) { 148 | $indexers[$key] = $allIndexers[$key]; 149 | $indexers[$key]->method = $method; 150 | } 151 | } 152 | return $indexers; 153 | } 154 | 155 | /** 156 | * Get all indexers 157 | * 158 | * @return array 159 | */ 160 | public function getAllIndexers() 161 | { 162 | $indexers = array(); 163 | $allIndexers = array(); 164 | $duplicates = array(); 165 | foreach ($this->getSchema()->getAllIndexers() as $indexer) { 166 | if (!$this->_validate($indexer)) { 167 | continue; 168 | } 169 | if (!isset($indexer->duplicates)) { 170 | $key = $indexer->fieldName.'/'.$indexer->indexerName; 171 | $allIndexers[$key] = $indexers[$key] = $indexer; 172 | } else { 173 | $duplicates[$indexer->duplicates] = $indexer->method; 174 | } 175 | } 176 | foreach ($duplicates as $key => $method) { 177 | if (isset($allIndexers[$key])) { 178 | $indexers[$key] = $allIndexers[$key]; 179 | $indexers[$key]->method = $method; 180 | } 181 | } 182 | return $indexers; 183 | } 184 | 185 | /** 186 | * Validate indexer configuration 187 | * 188 | * @param Mage_Core_Model_Config_Element $indexer 189 | * @return bool 190 | * @throws Exception 191 | */ 192 | protected function _validate(stdClass $indexer) 193 | { 194 | try { 195 | 196 | if (!isset($indexer->indexerName)) { 197 | throw new Exception('Indexer name is required.'); 198 | } 199 | if (!isset($indexer->fieldName)) { 200 | throw new Exception(sprintf('Changed field name is required. Indexer name: %s.', $indexer->indexerName)); 201 | } 202 | 203 | if (isset($indexer->duplicates)) { 204 | if (!isset($indexer->method)) { 205 | throw new Exception(sprintf('Duplicates indexer must specify method. Indexer name: %s.', $indexer->indexerName)); 206 | } 207 | } else { 208 | if (!isset($indexer->entity)) { 209 | throw new Exception('Source entity model is required.'); 210 | } 211 | if (!isset($indexer->resource)) { 212 | throw new Exception(sprintf('Resource model to update index for is required. Indexer name: %s.', $indexer->indexerName)); 213 | } 214 | if (!isset($indexer->reference)) { 215 | throw new Exception(sprintf('Reference field is required. Indexer name: %s.', $indexer->indexerName)); 216 | } 217 | if (!isset($indexer->index)) { 218 | throw new Exception(sprintf('Location to save the indexed data is required. Indexer name: %s.', $indexer->indexerName)); 219 | } 220 | } 221 | 222 | return TRUE; 223 | 224 | } catch (Exception $e) { 225 | Mage::logException($e); 226 | Mage::log(json_encode($indexer)); 227 | if (Mage::getIsDeveloperMode()) { 228 | throw $e; 229 | } 230 | } 231 | 232 | return FALSE; 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /code/Model/Indexer/Schema.php: -------------------------------------------------------------------------------- 1 | setCacheId(self::CACHE_KEY); 18 | $this->setCacheTags(array(self::CACHE_TAG)); 19 | parent::__construct($sourceData); 20 | 21 | if (Mage::app()->useCache(self::CACHE_KEY)) { 22 | $this->setCache(Mage::app()->getCache()); 23 | if ($this->loadCache()) { 24 | return; 25 | } 26 | } 27 | 28 | $config = Mage::getConfig()->loadModulesConfiguration('mongo_indexer.xml'); 29 | $this->setXml($config->getNode()); 30 | 31 | if (Mage::app()->useCache(self::CACHE_KEY)) { 32 | $this->saveCache(); 33 | } 34 | } 35 | 36 | /** 37 | * Get indexers for the entity 38 | * 39 | * @param string $entity 40 | * @return array 41 | */ 42 | public function getEntityIndexers($entity) 43 | { 44 | $indexers = array(); 45 | if ($this->getNode('indexer')) { 46 | foreach ($this->getNode('indexer')->{$entity} as $node) { 47 | $indexers = array_merge($indexers, $this->_getEntityTypeNodeIndexers($node)); 48 | } 49 | } 50 | return $indexers; 51 | } 52 | 53 | /** 54 | * Get all indexers 55 | * 56 | * @return array 57 | */ 58 | public function getAllIndexers() 59 | { 60 | $indexers = array(); 61 | if ($this->getNode('indexer')) { 62 | foreach ($this->getNode('indexer')->children() as $key => $node) { 63 | $indexers = array_merge($indexers, $this->_getEntityTypeNodeIndexers($node)); 64 | } 65 | } 66 | return $indexers; 67 | } 68 | 69 | /** 70 | * Get entity type indexers 71 | * 72 | * @param Mage_Core_Model_Config_Element $node 73 | * @return array 74 | */ 75 | protected function _getEntityTypeNodeIndexers(Mage_Core_Model_Config_Element $node) 76 | { 77 | $indexers = array(); 78 | foreach ($node as $fieldName => $fieldNode) { 79 | foreach ($fieldNode as $indexerName => $indexerNode) { 80 | $indexer = (object) $indexerNode->asCanonicalArray(); 81 | $indexer->fieldName = $fieldName; 82 | $indexer->indexerName = $indexerName; 83 | if (!isset($indexer->ignore)) { 84 | $indexers[] = $indexer; 85 | } 86 | } 87 | } 88 | return $indexers; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /code/Model/Job.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * 11 | * 12 | * singleton 13 | * foo/bar 14 | * runQueue 15 | * _id 16 | * 3 17 | * 18 | * +3 min, +1 hour, +12 hours 19 | * 20 | * 21 | * 50,60,100 22 | * 23 | * keep 24 | * delete 25 | * 26 | * foo_queue.log 27 | * 28 | * false 29 | * foo,bar 30 | * 31 | * 32 | * 33 | * helper 34 | * ... 35 | * 36 | * 37 | * 38 | * 39 | * 40 | * 41 | * 42 | * 43 | * 44 | * 45 | * 46 | * Sample job method: 47 | * 48 | * public function downloadFooJob(Varien_Object $data, Cm_Mongo_Model_Job $job) 49 | * { 50 | * if( ! $data->getFoo()) { 51 | * $job->denyRetry(); 52 | * throw new Exception('No foo!'); 53 | * } 54 | * $foo = file_get_contents($foo); 55 | * if( ! $foo) { 56 | * throw new Exception('Could not download foo.'); 57 | * } 58 | * // Do something with $foo 59 | * } 60 | * 61 | * Document Fields: 62 | * _id 63 | * task 64 | * job_data 65 | * status 66 | * status_message 67 | * pid 68 | * retries 69 | * priority 70 | * execute_at 71 | * error_log 72 | * other_data 73 | * 74 | * @method Cm_Mongo_Model_Mongo_Job getResource() 75 | */ 76 | class Cm_Mongo_Model_Job extends Cm_Mongo_Model_Abstract 77 | { 78 | 79 | const STATUS_READY = 'ready'; 80 | const STATUS_RUNNING = 'running'; 81 | const STATUS_INVALID = 'invalid'; 82 | const STATUS_DISABLED = 'disabled'; 83 | const STATUS_SUCCESS = 'success'; 84 | const STATUS_FAILED = 'failed'; 85 | 86 | const LOGFILE = 'mongo_queue_errors.txt'; 87 | 88 | const DEFAULT_PRIORITY = 50; 89 | 90 | protected function _construct() 91 | { 92 | $this->_init('mongo/job'); 93 | } 94 | 95 | /** 96 | * @return boolean 97 | */ 98 | public function isTaskDisabled() 99 | { 100 | // Allow override 101 | if($this->getForceEnabled()) { 102 | return false; 103 | } 104 | // Check specific task config 105 | if($this->getTaskConfig()->is('disabled')) { 106 | return true; 107 | } 108 | // Check group config if a group is specified 109 | if($groups = (string) $this->getTaskConfig('groups')) { 110 | foreach(explode(',',$groups) as $group) { 111 | if(Mage::app()->getConfig()->getNode('mongo_queue/groups/'.$group)->is('disabled')) { 112 | return true; 113 | } 114 | } 115 | } 116 | return false; 117 | } 118 | 119 | /** 120 | * @param string $node 121 | * @return Mage_Core_Model_Config_Element 122 | */ 123 | public function getTaskConfig($node = '') 124 | { 125 | $node = 'mongo_queue/tasks/'.$this->getTask().($node ? "/$node" : ''); 126 | return Mage::app()->getConfig()->getNode($node); 127 | } 128 | 129 | /** 130 | * @param string $status 131 | * @param string|NULL $statusMessage 132 | * @return Cm_Mongo_Model_Job 133 | */ 134 | public function updateStatus($status, $statusMessage = NULL) 135 | { 136 | $this->setStatus($status) 137 | ->setStatusMessage($statusMessage) 138 | ->setPid(NULL); 139 | return $this; 140 | } 141 | 142 | /** 143 | * Ensure job will not be retried on failure even if "retries" config has not been reached. 144 | * 145 | * @return Cm_Mongo_Model_Job 146 | */ 147 | public function denyRetry() 148 | { 149 | return $this->setCanRetry(FALSE); 150 | } 151 | 152 | /** 153 | * Check if the job can be scheduled for retry on failure. Can be overridden witth denyRetry(). 154 | * 155 | * @return boolean 156 | */ 157 | public function canRetry() 158 | { 159 | if($this->hasData('can_retry')) { 160 | return $this->getCanRetry(); 161 | } 162 | $maxRetries = (int) $this->getTaskConfig('retries'); 163 | return $maxRetries && $maxRetries > $this->getRetries(); 164 | } 165 | 166 | /** 167 | * Schedule a retry based on the parameters or the task configuration. 168 | * This is automatically called if an exception is thrown and canRetry() is true. 169 | * 170 | * @param null|string|int|MongoDate $time A strtotime compatible string, a unix timestamp or a MongoDate 171 | * @return Cm_Mongo_Model_Job 172 | */ 173 | public function scheduleRetry($time = NULL) 174 | { 175 | $retries = (int) $this->getRetries(); 176 | 177 | // Update the execute_at time 178 | if($time === NULL) { 179 | $time = new MongoDate; 180 | $schedule = (string) $this->getTaskConfig('retry_schedule'); 181 | if($schedule) { 182 | $schedule = preg_split('/\s*,\s*/', $schedule, null, PREG_SPLIT_NO_EMPTY); 183 | $modifier = isset($schedule[$retries]) ? $schedule[$retries] : end($schedule); 184 | $time = new MongoDate(strtotime($modifier, $time->sec)); 185 | } 186 | } 187 | else if(is_string($time)) { 188 | $time = new MongoDate(strtotime($time)); 189 | } 190 | else if(is_int($time)) { 191 | $time = new MongoDate($time); 192 | } 193 | 194 | // Update the priority unless it was already set explicitly 195 | if( ! $this->dataHasChangedFor('priority')) { 196 | $priorities = (string) $this->getTaskConfig('priorities'); 197 | $priorities = preg_split('/\s*,\s*/', $priorities, null, PREG_SPLIT_NO_EMPTY); 198 | if(count($priorities) > 1) { 199 | $this->setPriority(isset($priorities[$retries+1]) ? $priorities[$retries+1] : end($priorities)); 200 | } 201 | } 202 | 203 | // Reset to ready status 204 | $this->updateStatus(self::STATUS_READY) 205 | ->op('$inc', 'retries', 1) 206 | ->setExecuteAt($time); 207 | return $this; 208 | } 209 | 210 | /** 211 | * @param string|Exception $e 212 | * @return Cm_Mongo_Model_Job 213 | */ 214 | public function logError($e) 215 | { 216 | if($this->getTaskConfig()->is('logging')) 217 | { 218 | $logfile = (string) $this->getTaskConfig('logfile'); 219 | if( ! $logfile) { 220 | $logfile = self::LOGFILE; 221 | } 222 | $message = "PID:{$this->getPid()}, Retries:{$this->getRetries()}, ". 223 | "Task:{$this->getTask()}, Data:".json_encode($this->getJobData())."\n". 224 | "Error: $e"; 225 | Mage::log($message, Zend_Log::DEBUG, $logfile); 226 | } 227 | $this->op('$push', 'error_log', array('time' => new MongoDate(), 'message' => "$e")); 228 | return $this; 229 | } 230 | 231 | /** 232 | * Executes the task and updates the status. 233 | * 234 | * @throws Exception on an unexpected configuration error. 235 | */ 236 | public function run() 237 | { 238 | if($this->getStatus() != self::STATUS_RUNNING) { 239 | throw new Exception('Cannot run job when status is not \'running\'.'); 240 | } 241 | 242 | // Mark invalid if task not defined 243 | if( ! $this->getTaskConfig('class')) { 244 | $this->updateStatus(self::STATUS_INVALID, 'Task not properly defined.') 245 | ->save(); 246 | return; 247 | } 248 | 249 | // If task is globally disabled, put job on hold. 250 | if($this->isTaskDisabled()) { 251 | $this->updateStatus(self::STATUS_DISABLED) 252 | ->save(); 253 | return; 254 | } 255 | 256 | // Load task 257 | switch((string) $this->getTaskConfig('type')) { 258 | case 'helper': 259 | $object = Mage::helper((string)$this->getTaskConfig('class')); 260 | break; 261 | case 'model': 262 | $object = Mage::getModel((string)$this->getTaskConfig('class')); 263 | break; 264 | case 'resource': 265 | $object = Mage::getResourceSingleton((string)$this->getTaskConfig('class')); 266 | break; 267 | case 'singleton': 268 | default: 269 | $object = Mage::getSingleton((string)$this->getTaskConfig('class')); 270 | break; 271 | } 272 | if( ! $object) { 273 | $this->updateStatus(self::STATUS_INVALID, 'Could not get task model.') 274 | ->save(); 275 | return; 276 | } 277 | 278 | // Confirm existence of method 279 | $method = (string) $this->getTaskConfig('method'); 280 | if( ! method_exists($object, $method)) { 281 | $this->updateStatus(self::STATUS_INVALID, 'Cannot call task method.') 282 | ->save(); 283 | return; 284 | } 285 | 286 | $data = new Varien_Object($this->getJobData()); 287 | 288 | // Run task 289 | try { 290 | 291 | // Load object if a load_index is specified 292 | if($this->getTaskConfig('type') == 'model' && ($loadIndex = $this->getTaskConfig('load_index'))) { 293 | if( ! $data->hasData($loadIndex)) { 294 | throw new Exception('No id in job data to load by.'); 295 | } 296 | $object->load($data->getData($loadIndex), $loadIndex); 297 | $data->unsetData($loadIndex); 298 | if( ! $object->getId()) { 299 | throw new Exception('Object could not be loaded.'); 300 | } 301 | } 302 | 303 | $object->$method($data, $this); 304 | 305 | // If status not changed by task, assume success 306 | if($this->getStatus() == self::STATUS_RUNNING || $this->getStatus() == self::STATUS_SUCCESS) { 307 | // Update or delete? 308 | if($this->getTaskConfig()->is('after_success','keep')) { 309 | $this->updateStatus(self::STATUS_SUCCESS); 310 | } 311 | // Delete by default 312 | else { 313 | $this->isDeleted(true); 314 | } 315 | } 316 | } 317 | 318 | // If exception caught, retry if possible 319 | catch(Exception $e) { 320 | if($this->getStatus() == self::STATUS_RUNNING) { 321 | $this->logError($e); 322 | 323 | // Retry 324 | if($this->canRetry()) { 325 | $this->scheduleRetry(); 326 | } 327 | // Delete if specified 328 | else if($this->getTaskConfig()->is('after_failure','delete')) { 329 | $this->isDeleted(true); 330 | } 331 | // Keep by default 332 | else { 333 | $this->updateStatus(self::STATUS_FAILED, $e->getMessage()); 334 | } 335 | } 336 | } 337 | $this->save(); 338 | } 339 | 340 | /** 341 | * Method for running via Magento cron 342 | */ 343 | public function runCron() 344 | { 345 | if( ! Mage::getStoreConfigFlag('system/mongo_queue/cron_enabled')) { 346 | return; 347 | } 348 | $limit = (int) Mage::getStoreConfig('system/mongo_queue/limit'); 349 | $time = (int) Mage::getStoreConfig('system/mongo_queue/time'); 350 | Mage::helper('mongo/queue')->runQueue($limit, $time); 351 | } 352 | 353 | protected function _beforeSave() 354 | { 355 | // Make sure a priority is set 356 | if($this->getData('priority') === null) { 357 | $priorities = $this->getTaskConfig('priorities'); 358 | if($priorities === false) { 359 | $priority = Cm_Mongo_Model_Job::DEFAULT_PRIORITY; 360 | } else { 361 | $priorities = preg_split('/\s*,\s*/', $priorities, null, PREG_SPLIT_NO_EMPTY); 362 | $priority = $priorities[0]; 363 | } 364 | $this->setData('priority', (int) $priority); 365 | } 366 | 367 | // Ensure that status updates are consistent 368 | if($this->isObjectNew() === false && $this->dataHasChangedFor('status')) { 369 | $this->setAdditionalSaveCriteria(array('status' => $this->getOrigData('status'))); 370 | } 371 | } 372 | 373 | protected function _afterSave() 374 | { 375 | // Throw errors if a status update fails 376 | if($this->getLastUpdateStatus() === false) { 377 | throw new Exception("Failed to update job status to {$this->getStatus()} ({$this->getId()})"); 378 | } 379 | } 380 | 381 | } 382 | -------------------------------------------------------------------------------- /code/Model/Mongo/Enum.php: -------------------------------------------------------------------------------- 1 | _init('mongo/enum'); 11 | } 12 | 13 | public function getDefaultLoadFields($object) 14 | { 15 | $fields = array( 16 | 'name' => 1, 17 | 'defaults' => 1 18 | ); 19 | if($object->getStoreId()) { 20 | $fields['stores.'.Mage::app()->getStore($object->getStoreId())->getCode()] = 1; 21 | } 22 | return $fields; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /code/Model/Mongo/Enum/Collection.php: -------------------------------------------------------------------------------- 1 | _init('mongo/enum'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /code/Model/Mongo/Fixture.php: -------------------------------------------------------------------------------- 1 | getCollectionName($collectionEntity); 23 | } 24 | if( ! isset($this->_collections[$collectionEntity])) { 25 | $this->_collections[$collectionEntity] = $this->_getWriteAdapter()->selectCollection($collectionEntity); 26 | } 27 | return $this->_collections[$collectionEntity]; 28 | } 29 | 30 | /** 31 | * @param string $collection 32 | */ 33 | public function cleanCollection($collection) 34 | { 35 | $this->_getCollection($collection)->remove_safe(array()); 36 | } 37 | 38 | /** 39 | * @param string $collection 40 | * @param array $data 41 | */ 42 | public function loadCollectionData($collection, $data) 43 | { 44 | $this->_getCollection($collection)->batchInsert($data); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /code/Model/Mongo/Indexer.php: -------------------------------------------------------------------------------- 1 | _init('mongo/indexer'); 19 | } 20 | 21 | /** 22 | * Reindex all data 23 | */ 24 | public function reindexAll() 25 | { 26 | foreach (Mage::getModel('mongo/indexer')->getAllIndexers() as $indexer) { 27 | $this->removeIndex($indexer); 28 | $collection = $this->getCollection($indexer); 29 | while ($model = $collection->getNextItem()) { /** @var $model Cm_Mongo_Model_Abstract */ 30 | $this->updateIndex($indexer, $model); 31 | $model->clearInstance(); 32 | } 33 | } 34 | } 35 | 36 | /** 37 | * @param stdClass $indexer 38 | * @param Mage_Core_Model_Abstract $model 39 | */ 40 | public function updateIndex(stdClass $indexer, Mage_Core_Model_Abstract $model) 41 | { 42 | if ($resource = $this->_getTargetResource($indexer)) { 43 | $resource->update( 44 | array($indexer->reference => $model->getId()), 45 | '$set', 46 | '_.'.$indexer->index, 47 | $this->_getValue($indexer, $model), 48 | array('multiple' => TRUE) 49 | ); 50 | } 51 | } 52 | 53 | /** 54 | * Remove index for the specified or all entities. 55 | * 56 | * @param stdClass $indexer 57 | * @param array|int $id 58 | */ 59 | public function removeIndex(stdClass $indexer, $id = null) 60 | { 61 | $criteria = array(); 62 | if (is_int($id)) { 63 | $criteria = array($indexer->reference => $id); 64 | } elseif (is_array($id)) { 65 | $criteria = array($indexer->reference => array('$in' => $id)); 66 | } 67 | if ($resource = $this->_getTargetResource($indexer)) { 68 | $resource->update( 69 | $criteria, 70 | '$unset', 71 | array('_.'.$indexer->index => 1), 72 | NULL, 73 | array('multiple' => TRUE) 74 | ); 75 | } 76 | } 77 | 78 | /** 79 | * @param stdClass $indexer 80 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 81 | */ 82 | public function getCollection(stdClass $indexer) 83 | { 84 | /** @var $collection Cm_Mongo_Model_Resource_Collection_Abstract */ 85 | $collection = Mage::getModel($indexer->entity)->getCollection(); 86 | // Add filters 87 | if (isset($indexer->filters)) { 88 | $this->_addFilters($collection, $indexer->filters); 89 | } 90 | // Add fields to selection 91 | if (isset($indexer->fields)) { 92 | $collection->addFieldToResults(array_values($indexer->fields)); 93 | } 94 | // Add fields to preload 95 | if (isset($indexer->dependency)) { 96 | $collection->addFieldToPreload(array_values($indexer->dependency)); 97 | } 98 | 99 | return $collection; 100 | } 101 | 102 | /** 103 | * @param Cm_Mongo_Model_Resource_Collection_Abstract $collection 104 | * @param array $filters 105 | */ 106 | protected function _addFilters(Cm_Mongo_Model_Resource_Collection_Abstract $collection, array $filters) 107 | { 108 | foreach ($filters as $filter) { 109 | $field = $filter['field']; 110 | $filterType = isset($filter['type']) ? $filter['type'] : self::FILTER_TYPE_STRING; 111 | switch ($filterType) { 112 | case self::FILTER_TYPE_BOOLEAN : 113 | $value = (bool) $filter['value']; 114 | break; 115 | case self::FILTER_TYPE_INTEGER : 116 | $value = intval($filter['value']); 117 | break; 118 | case self::FILTER_TYPE_ARRAY : 119 | $value = explode(',', $filter['value']); 120 | break; 121 | case self::FILTER_TYPE_STRING : 122 | default: 123 | $value = strval($filter['value']); 124 | break; 125 | } 126 | $condition = isset($filter['condition']) ? $filter['condition'] : 'eq'; 127 | // Add filter to collection 128 | $collection->addFieldToFilter($field, $condition, $value); 129 | } 130 | } 131 | 132 | /** 133 | * @param stdClass $indexer 134 | * @return Cm_Mongo_Model_Resource_Abstract|bool 135 | * @throws Exception 136 | */ 137 | protected function _getTargetResource(stdClass $indexer) 138 | { 139 | $resource = Mage::getResourceSingleton($indexer->resource); 140 | if (!$resource instanceof Cm_Mongo_Model_Resource_Abstract) { 141 | if (Mage::getIsDeveloperMode()) { 142 | throw new Exception(sprintf('Unknown resource model with the name: %s.', $indexer->resource)); 143 | } 144 | return FALSE; 145 | } 146 | return $resource; 147 | } 148 | 149 | /** 150 | * Get field value 151 | * 152 | * @param stdClass $indexer 153 | * @param Mage_Core_Model_Abstract $model 154 | * @return mixed 155 | */ 156 | protected function _getValue(stdClass $indexer, Mage_Core_Model_Abstract $model) 157 | { 158 | $value = (!empty($indexer->method) && is_callable(array($model, $indexer->method))) 159 | ? $model->{$indexer->method}() 160 | : $model->getData($indexer->changedFieldName); 161 | return $value; 162 | } 163 | } -------------------------------------------------------------------------------- /code/Model/Mongo/Job.php: -------------------------------------------------------------------------------- 1 | _init('mongo/job'); 13 | } 14 | 15 | /** 16 | * @return Cm_Mongo_Model_Job|bool FALSE if no job ready in queue 17 | */ 18 | public function getNextJob() 19 | { 20 | $result = $this->_getWriteAdapter()->command_safe(array( 21 | 'findAndModify' => $this->_collectionName, 22 | 'query' => array( 23 | 'status' => Cm_Mongo_Model_Job::STATUS_READY, 24 | 'execute_at' => array('$lte' => new MongoDate), 25 | ), 26 | 'sort' => array( 27 | 'priority' => 1, 28 | 'execute_at' => 1, 29 | ), 30 | 'update' => array( 31 | '$set' => array( 32 | 'status' => Cm_Mongo_Model_Job::STATUS_RUNNING, 33 | 'pid' => getmypid(), 34 | ), 35 | ), 36 | 'fields' => $this->getDefaultLoadFields(new Varien_Object), 37 | 'new' => true, 38 | )); 39 | if( empty($result['value'])) { 40 | return FALSE; 41 | } 42 | 43 | $data = $result['value']; 44 | $job = Mage::getModel('mongo/job'); 45 | $this->hydrate($job, $data, TRUE); 46 | return $job; 47 | } 48 | 49 | /** 50 | * Attempts to reload a job using findAndModify so as to safely reserve the job 51 | * to be run by the current process 52 | * 53 | * @param Cm_Mongo_Model_Job $job 54 | * @return boolean 55 | */ 56 | public function reloadJobForRunning(Cm_Mongo_Model_Job $job) 57 | { 58 | $result = $this->_getWriteAdapter()->command(array( 59 | 'findAndModify' => $this->_collectionName, 60 | 'query' => array( 61 | '_id' => $job->getId(), 62 | 'status' => Cm_Mongo_Model_Job::STATUS_READY, 63 | ), 64 | 'update' => array( 65 | '$set' => array( 66 | 'status' => Cm_Mongo_Model_Job::STATUS_RUNNING, 67 | 'pid' => getmypid(), 68 | ), 69 | ), 70 | 'fields' => $this->getDefaultLoadFields(new Varien_Object()), 71 | 'new' => true, 72 | )); 73 | 74 | if( empty($result['ok'])) { 75 | return FALSE; 76 | } 77 | 78 | $data = $result['value']; 79 | $this->hydrate($job, $data, TRUE); 80 | return TRUE; 81 | } 82 | 83 | /** 84 | * @param array $criteria 85 | * @param array $update 86 | * @return bool 87 | */ 88 | public function pauseJobs($criteria, $update = array()) 89 | { 90 | $criteria['status'] = Cm_Mongo_Model_Job::STATUS_READY; 91 | $criteria['$atomic'] = TRUE; 92 | $update['status'] = Cm_Mongo_Model_Job::STATUS_DISABLED; 93 | return $this->update($criteria, $update, array('multiple' => TRUE)); 94 | } 95 | 96 | /** 97 | * @param array $criteria 98 | * @param array $update 99 | * @return bool 100 | */ 101 | public function resumeJobs($criteria, $update = array()) 102 | { 103 | $criteria['status'] = Cm_Mongo_Model_Job::STATUS_DISABLED; 104 | $update['status'] = Cm_Mongo_Model_Job::STATUS_READY; 105 | return $this->update($criteria, $update, array('multiple' => TRUE)); 106 | } 107 | 108 | public function getDefaultLoadFields($object) 109 | { 110 | if($object->getLoadErrorLog()) { 111 | return array(); 112 | } else { 113 | return array('error_log' => 0); 114 | } 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /code/Model/Mongo/Job/Collection.php: -------------------------------------------------------------------------------- 1 | _init('mongo/job'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /code/Model/Resource/Abstract.php: -------------------------------------------------------------------------------- 1 | _resourceModel = $resource[0]; 35 | $this->_entityName = $resource[1]; 36 | $this->_collectionName = $this->getSchema()->getCollectionName($this->_resourceModel, $this->_entityName); 37 | $this->_caches[] = new Cm_Mongo_Collection(); 38 | } 39 | 40 | /** 41 | * Get the db read connection. Currently does not support alternate resources 42 | * 43 | * @return Mongo_Database 44 | */ 45 | protected function _getReadAdapter() 46 | { 47 | return Mage::getSingleton('core/resource')->getConnection('mongo_read'); 48 | } 49 | 50 | /** 51 | * Get the db read collection instance. 52 | * 53 | * @return Mongo_Collection 54 | */ 55 | protected function _getReadCollection() 56 | { 57 | if( ! $this->_readCollection) { 58 | $this->_readCollection = $this->_getReadAdapter()->selectCollection($this->_collectionName); 59 | } 60 | return $this->_readCollection; 61 | } 62 | 63 | /** 64 | * Get the db write connection. Currently does not support alternate resources 65 | * 66 | * @return Mongo_Database 67 | */ 68 | protected function _getWriteAdapter() 69 | { 70 | return Mage::getSingleton('core/resource')->getConnection('mongo_write'); 71 | } 72 | 73 | /** 74 | * Get the db write collection instance. 75 | * 76 | * @return Mongo_Collection 77 | */ 78 | protected function _getWriteCollection() 79 | { 80 | if( ! $this->_writeCollection) { 81 | if($this->_getReadAdapter() == $this->_getWriteAdapter()) { 82 | $this->_writeCollection = $this->_getReadCollection(); 83 | } else { 84 | $this->_writeCollection = $this->_getWriteAdapter()->selectCollection($this->_collectionName); 85 | } 86 | } 87 | return $this->_writeCollection; 88 | } 89 | 90 | /** 91 | * Make connection public.. Avoid usage 92 | * 93 | * @return Mongo_Database 94 | */ 95 | public function getReadConnection() 96 | { 97 | return $this->_getReadAdapter(); 98 | } 99 | 100 | /** 101 | * Make collection public.. Avoid usage 102 | * 103 | * @return Mongo_Collection 104 | */ 105 | public function getReadCollection() 106 | { 107 | return $this->_getReadCollection(); 108 | } 109 | 110 | /** 111 | * In Mongo, there is always an _id column 112 | * 113 | * @return string 114 | */ 115 | public function getIdFieldName() 116 | { 117 | return '_id'; 118 | } 119 | 120 | /** 121 | * Get cached reference to mongo schema model 122 | * 123 | * @return Cm_Mongo_Model_Schema 124 | */ 125 | public function getSchema() 126 | { 127 | return Mage::getSingleton('mongo/schema'); 128 | } 129 | 130 | /** 131 | * Get the schema config element for the given (or current) entity. 132 | * 133 | * @param string $entityName 134 | * @return Mage_Core_Model_Config_Element 135 | */ 136 | public function getEntitySchema($entityName = NULL) 137 | { 138 | if($entityName === NULL) { 139 | return $this->getSchema()->getEntitySchema($this->_resourceModel, $this->_entityName); 140 | } 141 | if(strpos($entityName, '/') === FALSE) { 142 | return $this->getSchema()->getEntitySchema($this->_resourceModel, $entityName); 143 | } 144 | return $this->getSchema()->getEntitySchema($entityName); 145 | } 146 | 147 | /** 148 | * Get the actual collection name for the given entity 149 | * 150 | * Supports parameter formats: 151 | * 152 | * resourceModel/collection 153 | * collection (resourceModel assumed from instance) 154 | * 155 | * @param string $entityName 156 | * @return string 157 | */ 158 | public function getCollectionName($entityName = NULL) 159 | { 160 | if($entityName === NULL) { 161 | return $this->_collectionName; 162 | } 163 | if (strpos($entityName, '/') === FALSE) { 164 | return $this->getSchema()->getCollectionName($this->_resourceModel, $entityName); 165 | } 166 | return $this->getSchema()->getCollectionName($entityName); 167 | } 168 | 169 | /** 170 | * Get the field mappings from the schema for this resource 171 | * 172 | * @return Varien_Simplexml_Element 173 | */ 174 | public function getFieldMappings() 175 | { 176 | return $this->getSchema()->getFieldMappings($this->_resourceModel, $this->_entityName); 177 | } 178 | 179 | /** 180 | * Get the field mapping data for the given field name. 181 | * 182 | * @param string $field 183 | * @throws Exception 184 | * @return Varien_Simplexml_Element 185 | */ 186 | public function getFieldMapping($field) 187 | { 188 | $mapping = $this->getFieldMappings()->{$field}; 189 | if( ! $mapping) { 190 | throw new Exception("$this->_resourceModel/$this->_entityName does not have a field named $field."); 191 | } 192 | return $mapping; 193 | } 194 | 195 | /** 196 | * Get the field type 197 | * 198 | * @param string $field 199 | * @return string 200 | */ 201 | public function getFieldType($field) 202 | { 203 | $mapping = $this->getFieldMapping($field); 204 | return isset($mapping->type) ? (string) $mapping->type : 'string'; 205 | } 206 | 207 | /** 208 | * Get the embedded/referenced model name for the given field. 209 | * 210 | * @param string $field 211 | * @return string 212 | */ 213 | public function getFieldModelName($field) 214 | { 215 | if( ! isset($this->_fieldModels[$field])) { 216 | $this->_fieldModels[$field] = (string) $this->getFieldMapping($field)->model; 217 | } 218 | return $this->_fieldModels[$field]; 219 | } 220 | 221 | /** 222 | * Get an instance of an embedded/referenced model for the given field. 223 | * 224 | * @param string $field 225 | * @return Cm_Mongo_Model_Abstract 226 | */ 227 | public function getFieldModel($field) 228 | { 229 | return Mage::getModel($this->getFieldModelName($field)); 230 | } 231 | 232 | /** 233 | * Get the resource instance of an embedded/referenced model for the given field. 234 | * 235 | * @param string $field 236 | * @return Cm_Mongo_Model_Resource_Abstract 237 | */ 238 | public function getFieldResource($field) 239 | { 240 | return Mage::getResourceSingleton($this->getFieldModelName($field)); 241 | } 242 | 243 | /** 244 | * Get the next auto-increment value for this resource entity 245 | * 246 | * @return int 247 | * @throws MongoException 248 | */ 249 | public function getAutoIncrement() 250 | { 251 | return $this->_getWriteAdapter()->get_auto_increment($this->_resourceModel.'/'.$this->_entityName); 252 | } 253 | 254 | /** 255 | * Set created_at and/or update_at timestamps on the given object 256 | * 257 | * @param Mage_Core_Model_Abstract $object 258 | * @param boolean $created 259 | * @param boolean $updated 260 | */ 261 | public function setTimestamps(Mage_Core_Model_Abstract $object, $created = TRUE, $updated = TRUE) 262 | { 263 | if($created) { 264 | $object->setData('created_at', new MongoDate()); 265 | } 266 | if($updated) { 267 | $object->setData('updated_at', new MongoDate()); 268 | } 269 | } 270 | 271 | /** 272 | * Load an object 273 | * 274 | * If $value is an array it is treated as a field => value query 275 | * 276 | * @param Mage_Core_Model_Abstract $object 277 | * @param mixed $value 278 | * @param string $field field to load by (defaults to model id) 279 | * @return Cm_Mongo_Model_Abstract 280 | */ 281 | public function load(Mage_Core_Model_Abstract $object, $value, $field=null) 282 | { 283 | if(is_array($value) && is_null($field)) { 284 | $query = $value; 285 | } else if(is_null($field)) { 286 | $query = array($this->getIdFieldName() => $value); 287 | } else { 288 | $query = array($field => $value); 289 | } 290 | 291 | if ( ! is_null($value)) { 292 | $fields = $this->getDefaultLoadFields($object); 293 | $data = $this->getDocument($query, $fields); 294 | if ($data) { 295 | $this->hydrate($object, $data, TRUE); 296 | } else { 297 | $object->isObjectNew(TRUE); 298 | } 299 | } 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Overload to choose only specific fields to be loaded. 306 | * 307 | * @param Varien_Object $object 308 | * @return array 309 | */ 310 | public function getDefaultLoadFields($object) 311 | { 312 | return array(); // all fields 313 | } 314 | 315 | /** 316 | * Get the document for a load. 317 | * 318 | * @param array $query 319 | * @param array $fields 320 | * @return array 321 | */ 322 | public function getDocument($query, $fields = array()) 323 | { 324 | foreach($query as $field => $value) { 325 | if($field{0} != '$' && ! is_array($value)) { 326 | $query[$field] = $this->castToMongoNested($field, $value); 327 | } 328 | } 329 | return $this->_getReadCollection()->findOne($query, $fields); 330 | } 331 | 332 | /** 333 | * Save object 334 | * 335 | * @param Cm_Mongo_Model_Abstract|Mage_Core_Model_Abstract $object 336 | * @throws Mage_Core_Exception 337 | * @throws MongoCursorException 338 | * @return Cm_Mongo_Model_Resource_Abstract 339 | */ 340 | public function save(Mage_Core_Model_Abstract $object) 341 | { 342 | if ($object->isDeleted()) { 343 | return $object->delete(); 344 | } 345 | 346 | $this->_beforeSave($object); 347 | 348 | $object->setLastUpdateStatus(NULL); 349 | 350 | // TRUE, do insert 351 | if($object->isObjectNew()) 352 | { 353 | // Set created and updated timestamps 354 | $this->setTimestamps( $object, 355 | $this->getEntitySchema()->created_timestamp, 356 | $this->getEntitySchema()->updated_timestamp 357 | ); 358 | 359 | // Collect data for mongo 360 | $data = $this->dehydrate($object); 361 | $ops = $object->getPendingOperations(); 362 | 363 | // Set autoincrement 364 | if(empty($data['_id']) && $this->getEntitySchema()->autoincrement) { 365 | $data['_id'] = $this->getAutoIncrement(); 366 | } 367 | 368 | // Translate $set operations to simple insert data if possible 369 | if(isset($ops['$set'])) { 370 | foreach($ops['$set'] as $key => $value) { 371 | if(strpos($key, '.') === false) { 372 | $data[$key] = $value; 373 | unset($ops['$set'][$key]); 374 | } 375 | // @TODO - expand . delimited keys 376 | } 377 | if( ! count($ops['$set'])) { 378 | unset($ops['$set']); 379 | } 380 | } 381 | 382 | // Get insert options, merge default with instance-specific options 383 | $options = array('safe' => TRUE); 384 | if($additionalSaveOptions = $object->getAdditionalSaveOptions()) { 385 | unset($additionalSaveOptions['upsert'],$additionalSaveOptions['multiple']); 386 | $options = array_merge($options, $additionalSaveOptions); 387 | } 388 | 389 | // Insert document (throws exception on failure) 390 | $this->_getWriteCollection()->insert($data, $options); 391 | if( ! $object->hasData('_id') || $data['_id'] != $object->getData('_id')) { 392 | $object->setData('_id', $data['_id'])->setOrigData('_id', $data['_id']); 393 | } 394 | 395 | // Execute any pending operations 396 | if($ops) { 397 | $object->setLastUpdateStatus($this->update($object, $ops)); 398 | } 399 | // Inserts don't use additional save criteria 400 | else if($object->getAdditionalSaveCriteria()) { 401 | $object->setAdditionalSaveCriteria(); 402 | $object->setLastUpdateStatus(TRUE); 403 | } 404 | } 405 | 406 | // FALSE, do update 407 | else if($object->isObjectNew() === FALSE) 408 | { 409 | if( ! $object->getId()) { 410 | throw new Mage_Core_Exception('Cannot save existing object without id.'); 411 | } 412 | 413 | // Set updated timestamp only 414 | $this->setTimestamps( $object, 415 | FALSE, 416 | $this->getEntitySchema()->updated_timestamp 417 | ); 418 | 419 | // Collect data for mongo and update using atomic operators 420 | $data = $this->getDataChangesForUpdate($object); 421 | $ops = $object->getPendingOperations(); 422 | if(isset($ops['$set'])) { 423 | $ops['$set'] = array_merge($data, (array) $ops['$set']); 424 | } 425 | else if($data) { 426 | $ops['$set'] = $data; 427 | } 428 | 429 | // Undo unsets that are overridden by sets 430 | if(isset($ops['$unset']) && isset($ops['$set'])) { 431 | foreach($ops['$unset'] as $key => $value) { 432 | if(isset($ops['$set'][$key])) { 433 | unset($ops['$unset'][$key]); 434 | } 435 | } 436 | if( ! count($ops['$unset'])) { 437 | unset($ops['$unset']); 438 | } 439 | } 440 | 441 | if($ops) { 442 | $object->setLastUpdateStatus($this->update($object, $ops)); 443 | } 444 | } 445 | 446 | // Object status is not known, do upsert 447 | else 448 | { 449 | // Created timestamps not available on upsert 450 | $this->setTimestamps( $object, 451 | FALSE, 452 | $this->getEntitySchema()->updated_timestamp 453 | ); 454 | 455 | // Collect data for upsert. If no operations then use data, otherwise add data to $set operation 456 | $data = $this->dehydrate($object); 457 | if($ops = $object->getPendingOperations()) { 458 | if(isset($ops['$set'])) { 459 | $ops['$set'] = array_merge($data, (array) $ops['$set']); 460 | } 461 | else { 462 | $ops['$set'] = $data; 463 | } 464 | $data = $ops; 465 | } 466 | $result = $this->update($object, $data, array('upsert' => TRUE)); 467 | if($result instanceof MongoId) { 468 | $object->setData('_id', $result); 469 | } 470 | $object->setLastUpdateStatus(!!$result); 471 | } 472 | 473 | // Clear pending operations 474 | $object->resetPendingOperations(); 475 | $object->isObjectNew(FALSE); 476 | 477 | $this->_afterSave($object); 478 | 479 | return $this; 480 | } 481 | 482 | /** 483 | * Delete the object 484 | * 485 | * @param Mage_Core_Model_Abstract $object 486 | * @return Cm_Mongo_Model_Abstract 487 | */ 488 | public function delete(Mage_Core_Model_Abstract $object) 489 | { 490 | if($object->getId() !== NULL) { 491 | $this->_beforeDelete($object); 492 | $this->remove( 493 | array($this->getIdFieldName() => $object->getId()), 494 | array('justOne' => TRUE) 495 | ); 496 | $this->_afterDelete($object); 497 | } 498 | return $this; 499 | } 500 | 501 | /** 502 | * Load mongo data into a model using the schema mappings 503 | * 504 | * @param Mage_Core_Model_Abstract $object 505 | * @param array $data 506 | * @param boolean $original Is the data from the database? 507 | * @throws Exception 508 | */ 509 | public function hydrate(Mage_Core_Model_Abstract $object, array $data, $original = FALSE) 510 | { 511 | $converter = $this->getMongoToPhpConverter(); 512 | foreach($this->getFieldMappings() as $field => $mapping) 513 | { 514 | if( ! array_key_exists($field, $data)) { 515 | continue; 516 | } 517 | 518 | $value = $data[$field]; 519 | if( $value !== NULL ) { 520 | $type = isset($mapping->type) ? (string) $mapping->type : 'string'; 521 | if($type == 'embedded') { 522 | $model = $object->getDataUsingMethod($field); 523 | if( ! $model instanceof Cm_Mongo_Model_Abstract) { 524 | throw new Exception('Invalid embedded object instance for '.$field.'.'); 525 | } 526 | // Allow data to be hydrated from Varien_Object 527 | if($value instanceof Varien_Object) { 528 | $value = $value->getData(); 529 | } 530 | $model->getResource()->hydrate($model, $value, $original); 531 | } 532 | elseif($type == 'embeddedSet') { 533 | // Allow data to be hydrated from Varien_Data_Collection 534 | if($value instanceof Varien_Data_Collection) { 535 | $value = $value->walk('getData'); 536 | } 537 | /* @var $set Cm_Mongo_Model_Resource_Collection_Embedded */ 538 | if( ! $object->getData($field)) { 539 | $set = $object->getDataUsingMethod($field); 540 | foreach($value as $itemData) 541 | { 542 | $model = $set->getNewEmptyItem(); 543 | $model->getResource()->hydrate($model, $itemData, $original); 544 | $set->addItem($model); 545 | } 546 | } 547 | else { 548 | $set = $object->getData($field); 549 | foreach($value as $index => $itemData) 550 | { 551 | $item = $set->getItemById($index); 552 | if( ! $item) { 553 | $item = Mage::getModel((string)$mapping->model); 554 | } 555 | $item->getResource()->hydrate($item, $itemData, $original); 556 | } 557 | } 558 | } 559 | else { 560 | $value = $converter->$type($mapping, $value); 561 | $object->setDataUsingMethod($field, $value); 562 | } 563 | } 564 | else { 565 | $object->setData($field, NULL); 566 | } 567 | } 568 | 569 | if($original) { 570 | $object->isObjectNew(FALSE); 571 | $object->setOrigData(); 572 | $this->_afterLoad($object); 573 | } 574 | } 575 | 576 | /** 577 | * Extract data from model into Mongo-compatible array 578 | * 579 | * @param Varien_Object $object 580 | * @param boolean $forUpdate 581 | * @return array 582 | */ 583 | public function dehydrate(Varien_Object $object, $forUpdate = FALSE) 584 | { 585 | $rawData = array(); 586 | $converter = $this->getPhpToMongoConverter(); 587 | foreach($this->getFieldMappings() as $field => $mapping) 588 | { 589 | if( ! $object->hasData($field) && ! isset($mapping->required)) { 590 | continue; 591 | } 592 | 593 | $rawValue = $object->getData($field); 594 | 595 | // Skip fields that have not changed on updates 596 | if( 597 | $forUpdate && 598 | ( ! $object->hasData($field) || $rawValue === $object->getOrigData($field)) && 599 | ! $rawValue instanceof Cm_Mongo_Model_Abstract && 600 | ! $rawValue instanceof Cm_Mongo_Model_Resource_Collection_Embedded 601 | ) { 602 | continue; 603 | } 604 | 605 | if($rawValue === NULL && isset($mapping->required) && ! isset($mapping->required->null)) { 606 | $rawValue = (string) $mapping->required; 607 | } 608 | 609 | $type = isset($mapping->type) ? (string) $mapping->type : 'string'; 610 | 611 | // Embedded object 612 | if($type == 'embedded') 613 | { 614 | if($rawValue instanceof Cm_Mongo_Model_Abstract) { 615 | $value = $rawValue->getResource()->dehydrate($rawValue, $forUpdate); 616 | } 617 | else { 618 | $value = NULL; 619 | } 620 | if($forUpdate && empty($value)) { 621 | continue; 622 | } 623 | } 624 | 625 | // Sets of embedded objects 626 | else if($type == 'embeddedSet') 627 | { 628 | $value = array(); 629 | if($rawValue instanceof Cm_Mongo_Model_Resource_Collection_Embedded) { 630 | $items = $rawValue->getItems(); 631 | } 632 | else { 633 | $items = (array) $rawValue; 634 | } 635 | $index = 0; 636 | foreach($items as $item) { 637 | if($item instanceof Cm_Mongo_Model_Abstract) { 638 | $_value = $item->getResource()->dehydrate($item, $forUpdate); 639 | } 640 | else if($item instanceof Varien_Object) { 641 | $_value = $item->getData(); 642 | } 643 | else { 644 | $_value = (array) $item; 645 | } 646 | if( ! $forUpdate || $_value) { 647 | $value[$index] = $_value; 648 | } 649 | $index++; 650 | } 651 | if($forUpdate && empty($value)) { 652 | continue; 653 | } 654 | } 655 | 656 | // Updating hashes should compute difference (does not unset missing keys!) 657 | else if($forUpdate && $type == 'hash' && $object->getOrigData($field)) 658 | { 659 | $value = array(); 660 | $data = $converter->$type($mapping, $rawValue); 661 | $origData = $object->getOrigData($field); 662 | foreach ($data as $k => $v) { 663 | if (!isset($origData[$k]) || $v !== $origData[$k]) { 664 | $value[$k] = $v; 665 | } 666 | } 667 | } 668 | 669 | // All other data types 670 | else { 671 | $value = $converter->$type($mapping, $rawValue); 672 | } 673 | 674 | $rawData[$field] = $value; 675 | } 676 | return $rawData; 677 | } 678 | 679 | /** 680 | * Update a document 681 | * 682 | * Arguments can take the following forms: 683 | * 684 | * $criteria, $update, $options 685 | * $criteria, $operation, $keysValues, $options 686 | * $criteria, $operation, $key, $value, $options 687 | * 688 | * And these less-preferred forms: 689 | * $criteria, $update, NULL, $options 690 | * $criteria, $update, NULL, NULL, $options 691 | * $criteria, $operation, $keysValues, NULL, $options 692 | * 693 | * @param array|Cm_Mongo_Model_Abstract|mixed $criteria A model instance, query array or an _id 694 | * @param array|string $update An update array or an update operation name 695 | * @param array|string|null $key Update options or a key to update 696 | * @param array|string|int|float|MongoDate|null $value Update options or the update value 697 | * @param array $options 698 | * @return boolean 699 | */ 700 | public function update($criteria, $update, $key = NULL, $value = NULL, $options = array()) 701 | { 702 | // Compress number of arguments required 703 | if(is_array($update) && is_array($key)) { 704 | $options = $key; 705 | } else if((is_array($update) || is_array($key)) && is_array($value)) { 706 | $options = $value; 707 | } 708 | 709 | // Prepare criteria 710 | if($criteria instanceof Cm_Mongo_Model_Abstract) { 711 | $idQuery = array($criteria->getIdFieldName() => $criteria->getId()); 712 | 713 | // Allow updates with additional criteria 714 | if($additionalCriteria = $criteria->getAdditionalSaveCriteria()) 715 | { 716 | $idQuery = array_merge($idQuery, $additionalCriteria); 717 | $criteria->setAdditionalSaveCriteria(); 718 | 719 | // Allow updates without _id field if additional criteria specified (upsert) 720 | if( ! $idQuery[$criteria->getIdFieldName()]) { 721 | unset($idQuery[$criteria->getIdFieldName()]); 722 | } 723 | } 724 | if($additionalOptions = $criteria->getAdditionalSaveOptions()) { 725 | $options = array_merge($options, $additionalOptions); 726 | } 727 | $criteria = $idQuery; 728 | } 729 | else if ( ! is_array($criteria)) { 730 | $criteria = array('_id' => $this->castToMongo('_id', $criteria)); 731 | } 732 | 733 | // Prepare update 734 | if(is_string($update)) { 735 | if(is_string($key)) { 736 | $update = array($update => array($key => $value)); 737 | } else { 738 | $update = array($update => $key); 739 | } 740 | } 741 | 742 | // Prepare options 743 | $options = array_merge(array('upsert' => FALSE, 'multiple' => FALSE, 'safe' => TRUE), $options); 744 | 745 | // Execute 746 | return $this->_getWriteCollection()->update_safe($criteria, $update, $options); 747 | } 748 | 749 | /** 750 | * Remove documents. 751 | * 752 | * Use "justOne" option to remove just one, defaults to multiple. 753 | * 754 | * @throws Exception 755 | * @param array $criteria 756 | * @param array $options 757 | * @return bool|int 758 | */ 759 | public function remove(array $criteria, $options = array()) 760 | { 761 | if( ! $criteria) { 762 | throw new Exception('Removal of all documents not allowed with helper.'); 763 | } 764 | return $this->_getWriteCollection()->remove_safe($criteria, $options); 765 | } 766 | 767 | /** 768 | * Get mongo to php data type converter instance 769 | * 770 | * @return Cm_Mongo_Model_Type_Tophp 771 | */ 772 | public function getMongoToPhpConverter() 773 | { 774 | return Mage::getSingleton('mongo/type_tophp'); 775 | } 776 | 777 | /** 778 | * Get php to mongo data type converter instance 779 | * 780 | * @return Cm_Mongo_Model_Type_Tomongo 781 | */ 782 | public function getPhpToMongoConverter() 783 | { 784 | return Mage::getSingleton('mongo/type_tomongo'); 785 | } 786 | 787 | /** 788 | * Cast a value to the proper type based on the schema's field mapping for this resource. 789 | * 790 | * @param string $field 791 | * @param mixed $value 792 | * @return mixed 793 | */ 794 | public function castToMongo($field, $value) 795 | { 796 | $mapping = $this->getFieldMapping($field); 797 | $type = $this->getFieldType($field); 798 | return $this->getPhpToMongoConverter()->$type($mapping, $value); 799 | } 800 | 801 | /** 802 | * Cast a value to the proper type, but with support for nested field names. 803 | * 804 | * @param string $field 805 | * @param mixed $value 806 | * @param string $delimiter 807 | * @return mixed 808 | */ 809 | public function castToMongoNested($field, $value, $delimiter = '.') 810 | { 811 | $fields = explode($delimiter, $field, 2); 812 | $mapping = $this->getFieldMappings()->{$fields[0]}; 813 | if($mapping && count($fields) == 1) { 814 | return $this->castToMongo($fields[0], $value); 815 | } 816 | if( ! $mapping || ! in_array($mapping->type, array('embedded','embeddedSet'))) { 817 | return $value; 818 | } 819 | return $this->getFieldResource($fields[0])->castToMongoNested($fields[1], $value, $delimiter); 820 | } 821 | 822 | /** 823 | * Require that the current value for the object for the given field be unique. 824 | * 825 | * @param Cm_Mongo_Model_Abstract $object 826 | * @param string $field 827 | * @throws Mage_Core_Exception if value is not unique 828 | */ 829 | public function requireUniqueValue(Cm_Mongo_Model_Abstract $object, $field) 830 | { 831 | $value = $object->getData($field); 832 | $query = array($field => $value); 833 | if($object->getId()) { 834 | $query['_id'] = array('$ne' => $object->getId()); 835 | } 836 | $result = $this->_getReadCollection()->findOne($query); 837 | if($result) { 838 | throw new Mage_Core_Exception('Duplicate value for field '.$field); 839 | } 840 | } 841 | 842 | /** 843 | * Get an array of changes. If $flat is true (default), the keys will be . delimited instead of nested 844 | * 845 | * @param Cm_Mongo_Model_Abstract $object 846 | * @param bool $flat 847 | * @return array 848 | */ 849 | public function getDataChangesForUpdate(Cm_Mongo_Model_Abstract $object, $flat = TRUE) 850 | { 851 | $data = $this->dehydrate($object, TRUE); 852 | if($flat) { 853 | return $this->_flattenUpdateData($object->getOrigData(), $data); 854 | } else { 855 | return $data; 856 | } 857 | } 858 | 859 | /** 860 | * Flatten a nested array of values to update into . delimited keys 861 | * 862 | * @param array $orig 863 | * @param array $data 864 | * @param string $path Used for recursion 865 | * @return array 866 | */ 867 | public function _flattenUpdateData($orig, $data, $path = '') 868 | { 869 | $result = array(); 870 | foreach($data as $key => $value) 871 | { 872 | if(is_array($value) && isset($orig[$key])) 873 | { 874 | $origData = $orig[$key]; 875 | 876 | // Use orig data from embedded objects 877 | if($origData instanceof Cm_Mongo_Model_Abstract && ! ($origData = $origData->getOrigData())) { 878 | $result[$path.$key] = $value; 879 | } 880 | // If orig embedded collection was replaced with new collection, set new array 881 | else if($origData instanceof Cm_Mongo_Model_Resource_Collection_Embedded && $origData->isReplaced()) { 882 | $result[$path.$key] = $value; 883 | } 884 | // Use orig data from embedded collections 885 | else if($origData instanceof Cm_Mongo_Model_Resource_Collection_Embedded) { 886 | $index = 0; 887 | foreach($origData as $item) { 888 | if(isset($value[$index])) { 889 | $itemOrigData = $item->getOrigData(); 890 | if($itemOrigData) { 891 | $result2 = $this->_flattenUpdateData($itemOrigData, $value[$index], $key.'.'.$index.'.'); 892 | foreach($result2 as $key2 => $value2) { 893 | $result[$path.$key2] = $value2; 894 | } 895 | } 896 | else { 897 | $result[$path.$key.'.'.$index] = $value[$index]; 898 | } 899 | } 900 | $index++; 901 | } 902 | } 903 | // Always overwrite numerically indexed arrays rather than set individual values 904 | else if(is_array($origData) && key($value) === 0) { 905 | $result[$path.$key] = $value; 906 | } 907 | // Recurse! 908 | else { 909 | $result2 = $this->_flattenUpdateData($origData, $value, $key.'.'); 910 | foreach($result2 as $key2 => $value2) { 911 | $result[$path.$key2] = $value2; 912 | } 913 | } 914 | } 915 | else 916 | { 917 | $result[$path.$key] = $value; 918 | } 919 | } 920 | return $result; 921 | } 922 | 923 | /* Optional callback placeholders */ 924 | protected function _afterLoad(Mage_Core_Model_Abstract $object){} 925 | protected function _beforeSave(Mage_Core_Model_Abstract $object){} 926 | protected function _afterSave(Mage_Core_Model_Abstract $object){} 927 | protected function _beforeDelete(Mage_Core_Model_Abstract $object){} 928 | protected function _afterDelete(Mage_Core_Model_Abstract $object){} 929 | 930 | /** @ignore */ 931 | public function beginTransaction(){} 932 | /** @ignore */ 933 | public function commit() 934 | { 935 | $adapterKey = spl_object_hash($this->_getWriteAdapter()); 936 | if (isset(self::$_commitCallbacks[$adapterKey])) { 937 | $callbacks = self::$_commitCallbacks[$adapterKey]; 938 | self::$_commitCallbacks[$adapterKey] = array(); 939 | foreach ($callbacks as $callback) { 940 | call_user_func($callback); 941 | } 942 | } 943 | } 944 | /** @ignore */ 945 | public function rollback(){} 946 | 947 | 948 | /* * * * * * * * * 949 | * Object Caching 950 | */ 951 | 952 | /** 953 | * Adds a collection to the resource's collection cache 954 | * 955 | * @param Cm_Mongo_Model_Resource_Collection_Abstract $collection 956 | */ 957 | public function addCollectionToCache(Cm_Mongo_Model_Resource_Collection_Abstract $collection) 958 | { 959 | $hash = spl_object_hash($collection); 960 | $this->_caches[$hash] = $collection; 961 | } 962 | 963 | /** 964 | * Removes a collection from the resource's collection cache 965 | * 966 | * @param Cm_Mongo_Model_Resource_Collection_Abstract $collection 967 | */ 968 | public function removeCollectionFromCache(Cm_Mongo_Model_Resource_Collection_Abstract $collection) 969 | { 970 | $hash = spl_object_hash($collection); 971 | unset($this->_caches[$hash]); 972 | } 973 | 974 | /** 975 | * Add an object to the object cache 976 | * 977 | * @param Cm_Mongo_Model_Abstract $object 978 | */ 979 | public function addObjectToCache(Cm_Mongo_Model_Abstract $object) 980 | { 981 | $this->_caches[0]->addItem($object); 982 | } 983 | 984 | /** 985 | * Remove an object from the cache 986 | * 987 | * @param Cm_Mongo_Model_Abstract $object 988 | */ 989 | public function removeObjectFromCache(Cm_Mongo_Model_Abstract $object) 990 | { 991 | $id = $this->_getItemKey($object->getId()); 992 | if($id !== NULL) { 993 | foreach($this->_caches as $collection) 994 | { 995 | $collection->removeItemByKey($id); 996 | } 997 | } 998 | } 999 | 1000 | /** 1001 | * Fetches an object from the resource's cache 1002 | * 1003 | * @param int|string|MongoId $id 1004 | * @return Cm_Mongo_Model_Abstract 1005 | */ 1006 | public function getCachedObject($id) 1007 | { 1008 | $id = $this->_getItemKey($id); 1009 | foreach($this->_caches as $collection) 1010 | { 1011 | /* @var Varien_Data_Collection $collection */ 1012 | if($item = $collection->getItemById($id)) { 1013 | return $item; 1014 | } 1015 | } 1016 | return NULL; 1017 | } 1018 | 1019 | /** 1020 | * Get a key for an item 1021 | * 1022 | * @param int|string|MongoId $id 1023 | * @return int|string 1024 | */ 1025 | protected function _getItemKey($id) 1026 | { 1027 | $id = $this->castToMongo('_id', $id); 1028 | if($id instanceof MongoId) { 1029 | return (string) $id; 1030 | } 1031 | return $id; 1032 | } 1033 | 1034 | } 1035 | -------------------------------------------------------------------------------- /code/Model/Resource/Collection/Abstract.php: -------------------------------------------------------------------------------- 1 | _construct(); 47 | $this->_resource = $resource; 48 | $this->_conn = $this->getResource()->getReadConnection(); 49 | $this->_query = $this->_conn->selectCollection($this->getCollectionName()); 50 | } 51 | 52 | /** 53 | * Overload to perform initialization 54 | */ 55 | public function _construct() 56 | { 57 | } 58 | 59 | /** 60 | * Standard resource collection initialization 61 | * 62 | * @param string $model 63 | * @param null $resourceModel 64 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 65 | */ 66 | protected function _init($model, $resourceModel=null) 67 | { 68 | $this->setItemObjectClass(Mage::getConfig()->getModelClassName($model)); 69 | if (is_null($resourceModel)) { 70 | $resourceModel = $model; 71 | } 72 | $this->_resourceModel = $resourceModel; 73 | return $this; 74 | } 75 | 76 | /** 77 | * Add aggregate data, such as from a group operation. 78 | * 79 | * $keys is in the form of: 80 | * '_id' => 'ref_id' (index in this collection, index in aggregate data) 81 | * 82 | * @param array $keys 83 | * @param array $data 84 | * @param array $defaults Optionally specify default values for missing data 85 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 86 | */ 87 | public function addAggregateData($keys, &$data, $defaults = array()) 88 | { 89 | // Multi-key join 90 | if(count($keys) > 1) { 91 | // todo, support adding aggregate data by multiple keys 92 | } 93 | 94 | // Single-key join 95 | else { 96 | $field = key($keys); 97 | $joinField = $keys[$field]; 98 | $index = array(); 99 | foreach($data as &$doc) { 100 | $key = $doc[$joinField]; 101 | unset($doc[$joinField]); 102 | $index[$key] = $doc; 103 | } 104 | foreach($this->getItems() as $item) { 105 | $id = $item->getData($field); 106 | if(isset($index[$id])) { 107 | $item->addData($index[$id]); 108 | } else if($defaults) { 109 | $item->addData($defaults); 110 | } 111 | } 112 | } 113 | return $this; 114 | } 115 | 116 | /** 117 | * Add field filter to collection. 118 | * 119 | * If $field is an array then each key => value is applied as a separate condition. 120 | * 121 | * If $field is '$or' or '$nor', each $condition is processed as part of the or/nor query. 122 | * 123 | * Filter by other collection value using the -> operator to separate the 124 | * field that references the other collection and the field in the other collection. 125 | * - ('bar_id->name', 'eq', 'Baz') 126 | * 127 | * If $_condition is not null and $condition is not an array, value will not be cast. 128 | * 129 | * If $condition is assoc array - one of the following structures is expected: 130 | * - array("from"=>$fromValue, "to"=>$toValue) 131 | * - array("eq|neq"=>$likeValue) 132 | * - array("like|nlike"=>$likeValue) 133 | * - array("null|notnull"=>TRUE) 134 | * - array("is"=>"NULL|NOT NULL") 135 | * - array("in|nin"=>$array) 136 | * - array("raw"=>$mixed) - No typecasting 137 | * - array("$__"=>$value) - Any mongo operator 138 | * 139 | * @param string $field 140 | * @param null|string|array $condition 141 | * @param null|string|array $_condition 142 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 143 | */ 144 | public function addFieldToFilter($field, $condition = NULL, $_condition = NULL) 145 | { 146 | switch (func_num_args()) { 147 | case 1: 148 | $query = $this->_getCondition($field); break; 149 | case 2: 150 | $query = $this->_getCondition($field, $condition); break; 151 | case 3: default: 152 | $query = $this->_getCondition($field, $condition, $_condition); break; 153 | } 154 | $this->_query->find($query); 155 | return $this; 156 | } 157 | 158 | /** 159 | * Adds a field to be preloaded after the collection is loaded. 160 | * If the collection is already loaded it will preload the referenced 161 | * collection immediately. 162 | * 163 | * Supports reference chains in field name using -> delimiter. 164 | * 165 | * @param string|array $field 166 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 167 | */ 168 | public function addFieldToPreload($field) 169 | { 170 | // Process arrays individually 171 | if(is_array($field)) { 172 | foreach($field as $_field) { 173 | $this->addFieldToPreload($_field); 174 | } 175 | return $this; 176 | } 177 | 178 | // If a field uses chaining 179 | if(strpos($field, '->')) 180 | { 181 | $parts = explode('->', $field); 182 | $collection = $this; 183 | while($_field = array_shift($parts)) { 184 | $collection = $collection->addFieldToPreload($_field)->getReferencedCollection($_field); 185 | } 186 | } 187 | 188 | // Otherwise 189 | else if( ! isset($this->_preloadFields[$field])) 190 | { 191 | $this->_preloadFields[$field] = TRUE; 192 | } 193 | 194 | if($this->isLoaded()) { 195 | $this->_loadReferences(); 196 | } 197 | return $this; 198 | } 199 | 200 | /** 201 | * Add (or remove) fields to query results. 202 | * 203 | * @param string|array $field 204 | * @param bool|int $include 205 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 206 | */ 207 | public function addFieldToResults($field, $include = 1) 208 | { 209 | $include = (int) $include; 210 | if( ! is_array($field)) { 211 | $field = array($field => $include); 212 | } 213 | else if($field == array_values($field)) { 214 | $field = array_fill_keys($field, $include); 215 | } 216 | 217 | foreach($field as $_field => $_include) { 218 | if( strpos($_field, '->') ) { 219 | list($fieldA, $fieldB) = explode('->', $_field, 2); 220 | $this->addFieldToResults($fieldA, 1); 221 | $this->getReferencedCollection($fieldA)->addFieldToResults($fieldB, $_include); 222 | } 223 | else { 224 | $this->_query->fields(array($_field => $_include)); 225 | } 226 | } 227 | 228 | return $this; 229 | } 230 | 231 | /** 232 | * Check if the query already has explicit field includes specified (include and exclude cannot be mixed) 233 | * 234 | * @return bool 235 | */ 236 | public function hasIncludedFields() 237 | { 238 | foreach($this->_query->get_option('fields') as $include) { 239 | if($include == 1) { 240 | return TRUE; 241 | } 242 | } 243 | return FALSE; 244 | } 245 | 246 | /** 247 | * Adds the collection to the resource cache after the collection is loaded 248 | * 249 | * @param boolean $now Force the collection to be added now even if it is not loaded 250 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 251 | */ 252 | public function addToCache($now = FALSE) 253 | { 254 | if( ($now || $this->isLoaded()) && ! isset($this->_addedToCache)) { 255 | $this->getResource()->addCollectionToCache($this); 256 | $this->_addedToCache = TRUE; 257 | } 258 | else { 259 | $this->_addToCacheAfterLoad = TRUE; 260 | } 261 | return $this; 262 | } 263 | 264 | /** 265 | * Cast values to the proper type before running query 266 | * 267 | * @param string $field 268 | * @param mixed $value 269 | * @return mixed 270 | */ 271 | public function castFieldValue($field, $value) 272 | { 273 | try { 274 | $resource = $this->getResource(); 275 | if(strpos($field, '.')) { 276 | $parts = explode('.', $field); 277 | $field = array_pop($parts); 278 | while ($_field = array_shift($parts)) { 279 | if (isset($resource->getFieldMapping($_field)->id_field)) { 280 | return $resource->castToMongo('_id', $value); 281 | } 282 | if (isset($resource->getFieldMapping($_field)->subtype)) { 283 | if( ! count($parts)) { 284 | $subtype = (string) $resource->getFieldMapping($_field)->subtype; 285 | return $resource->getPhpToMongoConverter()->$subtype((object)null, $value); 286 | } 287 | return $value; 288 | } 289 | $resource = $resource->getFieldResource($_field); 290 | } 291 | } 292 | return $resource->castToMongo($field, $value); 293 | } 294 | catch(Exception $e) { 295 | return $value; 296 | } 297 | } 298 | 299 | /** 300 | * Returns the referenced collection unloaded so that additional filters/parameters may be set. 301 | * 302 | * @param string $field 303 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 304 | */ 305 | public function getReferencedCollection($field) 306 | { 307 | if(empty($this->_referencedCollections[$field])) 308 | { 309 | if( ! ($fieldModelName = $this->getResource()->getFieldModelName($field))) { 310 | throw new Exception("Could not get field model for $field"); 311 | } 312 | $this->_referencedCollections[$field] = Mage::getSingleton($fieldModelName)->getCollection(); 313 | } 314 | return $this->_referencedCollections[$field]; 315 | } 316 | 317 | /** 318 | * Applies a filter to the given collection based on this collection's field values. 319 | * 320 | * @param string $field 321 | * @param Cm_Mongo_Model_Resource_Collection_Abstract $collection 322 | * @throws Exception 323 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 324 | */ 325 | public function applyReferencedCollectionFilter($field, $collection) 326 | { 327 | $ids = array(); 328 | 329 | // Get all ids for the given field 330 | switch ( (string) $this->getResource()->getFieldType($field)) 331 | { 332 | case 'reference': 333 | foreach ($this->getItems() as $item) { 334 | if ($ref = $item->getData($field)) { 335 | $ids[] = $ref; 336 | } 337 | } 338 | break; 339 | // Get unique set of ids from field 340 | case 'referenceSet': 341 | foreach ($this->getItems() as $item) { 342 | if ($refSet = $item->getData($field)) { 343 | foreach ($refSet as $ref) { 344 | $ids[] = $ref; 345 | } 346 | } 347 | } 348 | break; 349 | case 'referenceHash': 350 | $idField = (string) $this->getResource()->getFieldMapping($field)->id_field; 351 | if ( ! $idField) { 352 | throw new Exception("Field definition for $field is missing 'id_field' definition."); 353 | } 354 | foreach ($this->getItems() as $item) { 355 | if ($refSet = $item->getData($field)) { 356 | foreach ($refSet as $ref) { 357 | if ( ! empty($ref[$idField])) { 358 | $ids[] = $ref[$idField]; 359 | } 360 | } 361 | } 362 | } 363 | default: 364 | throw new Exception("Cannot get referenced collection for field '$field'."); 365 | } 366 | 367 | // array_unique is slow, but required for compatibility with MongoId and possibly other id data types 368 | $ids = array_unique($ids); 369 | 370 | // Instantiate a collection filtered to the referenced objects using $in 371 | $collection->addFieldToFilter('_id', '$in', array_values($ids)); 372 | return $this; 373 | } 374 | 375 | /** 376 | * Get resource instance 377 | * 378 | * @return Cm_Mongo_Model_Resource_Abstract 379 | */ 380 | public function getResource() 381 | { 382 | if (empty($this->_resource)) { 383 | $this->_resource = Mage::getResourceSingleton($this->_resourceModel); 384 | } 385 | return $this->_resource; 386 | } 387 | 388 | /** 389 | * Get the collection name for this collection resource model 390 | * 391 | * @param string $entityName The entity name to get the collection name for (defaults to current entity) 392 | * @return string 393 | */ 394 | public function getCollectionName($entityName = NULL) 395 | { 396 | return $this->getResource()->getCollectionName($entityName); 397 | } 398 | 399 | /** 400 | * The id field name (_id) 401 | * 402 | * @return string 403 | */ 404 | public function getIdFieldName() 405 | { 406 | return $this->getResource()->getIdFieldName(); 407 | } 408 | 409 | /** 410 | * Get the database connection 411 | * 412 | * @return Mongo_Database 413 | */ 414 | public function getConnection() 415 | { 416 | return $this->_conn; 417 | } 418 | 419 | /** 420 | * Get the mongo collection instance which is used as a query object 421 | * 422 | * @return Mongo_Collection 423 | */ 424 | public function getQuery() 425 | { 426 | return $this->_query; 427 | } 428 | 429 | /** 430 | * Get collection size (ignoring limit and skip) 431 | * 432 | * @return int 433 | */ 434 | public function getSize() 435 | { 436 | if (is_null($this->_totalRecords)) { 437 | $query = clone $this->getQuery(); 438 | $query->unset_option('sort'); // ignore sorting (not sure if this is necessary) 439 | $this->_totalRecords = $query->count(FALSE); // false ignores limit and skip 440 | } 441 | return intval($this->_totalRecords); 442 | } 443 | 444 | /** 445 | * Get all _id's for the current query. If $noLoad is true and the collection is not already loaded 446 | * then a query will be run returning only the _ids and no objects will be hydrated. 447 | * 448 | * @param boolean $noLoad 449 | * @return array 450 | */ 451 | public function getAllIds($noLoad = FALSE) 452 | { 453 | if ( ( ! $this->_curPage && ! $this->_pageSize) && $this->isLoaded() || ! $noLoad) { 454 | return parent::getAllIds(); 455 | } 456 | 457 | // Use fast method of getting ids, full documents not loaded 458 | $idsQuery = clone $this->_query; 459 | $idsQuery->set_option('fields', array('_id' => 1)); 460 | $idsQuery->unset_option('skip'); 461 | $idsQuery->unset_option('limit'); 462 | $ids = array(); 463 | foreach($idsQuery->cursor() as $document) { 464 | $ids[] = $document['_id']; 465 | } 466 | return $ids; 467 | } 468 | 469 | /** 470 | * Add cursor order 471 | * 472 | * @param string|array $field 473 | * @param string $direction 474 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 475 | */ 476 | public function setOrder($field, $direction = self::SORT_ORDER_DESC) 477 | { 478 | return $this->_setOrder($field, $direction); 479 | } 480 | 481 | /** 482 | * Add cursor order to the beginning 483 | * 484 | * @param string|array $field 485 | * @param string $direction 486 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 487 | */ 488 | public function unshiftOrder($field, $direction = self::SORT_ORDER_DESC) 489 | { 490 | return $this->_setOrder($field, $direction, true); 491 | } 492 | 493 | /** 494 | * Unset all applied sort orders 495 | * 496 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 497 | */ 498 | public function resetOrder() 499 | { 500 | $this->_query->unset_option('sort'); 501 | return $this; 502 | } 503 | 504 | /** 505 | * Load data 506 | * 507 | * @param boolean $printQuery 508 | * @param boolean $logQuery 509 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 510 | */ 511 | public function load($printQuery = false, $logQuery = false) 512 | { 513 | if ($this->isLoaded()) { 514 | return $this; 515 | } 516 | 517 | $documents = $this->getData(); 518 | $this->resetData(); 519 | 520 | if($printQuery || $logQuery) { 521 | $this->debugQuery($printQuery, $logQuery); 522 | } 523 | 524 | if (is_array($documents)) { 525 | foreach ($documents as $data) { 526 | $item = $this->getNewEmptyItem(); 527 | $this->getResource()->hydrate($item, $data, TRUE); 528 | $this->addItem($item); 529 | } 530 | } 531 | 532 | $this->_setIsLoaded(); 533 | $this->_loadReferences(); 534 | $this->_afterLoad(); 535 | return $this; 536 | } 537 | 538 | /** 539 | * Get all data array for collection 540 | * 541 | * @return array 542 | */ 543 | public function getData() 544 | { 545 | if ($this->_iterating) 546 | { 547 | throw new Exception('Cannot getData while iterating.'); 548 | } 549 | 550 | if ($this->_data === null) 551 | { 552 | $this->_beforeLoad(); 553 | $this->_renderLimit(); 554 | $this->_renderFields(); 555 | $this->_data = $this->_query->as_array(FALSE); 556 | $this->_afterLoadData(); 557 | } 558 | return $this->_data; 559 | } 560 | 561 | /** 562 | * This method cannot be used with load(), getData() or the default iterator 563 | * _beforeLoad and _afterLoadData are not called 564 | * 565 | * @return null|Cm_Mongo_Model_Abstract 566 | */ 567 | public function getNextItem() 568 | { 569 | if ( ! $this->_iterating) { 570 | if ($this->isLoaded() || $this->_data) { 571 | throw new Exception('Cannot use getNextItem after collection is loaded.'); 572 | } 573 | $this->_renderLimit(); 574 | $this->_renderFields(); 575 | $this->_iterating = TRUE; 576 | } 577 | $data = $this->_query->getNext(); 578 | if( ! $data) { 579 | $this->_iterating = FALSE; 580 | return NULL; 581 | } 582 | $item = $this->getNewEmptyItem(); 583 | $this->getResource()->hydrate($item, $data, TRUE); 584 | return $item; 585 | } 586 | 587 | /** 588 | * @param boolean $printQuery 589 | * @param boolean $logQuery 590 | * @return string 591 | */ 592 | public function debugQuery($printQuery = false, $logQuery = false) 593 | { 594 | $query = $this->getQuery()->inspect(); 595 | if ($printQuery) { 596 | echo $query; 597 | } 598 | if ($logQuery){ 599 | Mage::log($query); 600 | } 601 | return $query; 602 | } 603 | 604 | /** 605 | * Reset loaded for collection data array 606 | * 607 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 608 | */ 609 | public function resetData() 610 | { 611 | $this->_data = null; 612 | return $this; 613 | } 614 | 615 | /** 616 | * Reset collection to fresh state. 617 | * 618 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 619 | */ 620 | public function reset() 621 | { 622 | $this->getQuery()->reset(); 623 | $this->_setIsLoaded(false); 624 | $this->_iterating = FALSE; 625 | $this->_items = array(); 626 | $this->_data = null; 627 | $this->_preloadFields = array(); 628 | $this->_referencedCollections = array(); 629 | $this->getResource()->removeCollectionFromCache($this); 630 | return $this; 631 | } 632 | 633 | /** 634 | * Save all the entities in the collection 635 | * 636 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 637 | */ 638 | public function save() 639 | { 640 | foreach ($this->getItems() as $item) { 641 | $item->save(); 642 | } 643 | return $this; 644 | } 645 | 646 | /** 647 | * Overridden to set correct id field name 648 | * 649 | * @return array 650 | */ 651 | public function toOptionArray() 652 | { 653 | return $this->_toOptionArray('_id', 'name'); 654 | } 655 | 656 | /** 657 | * Overridden to set correct id field name 658 | * 659 | * @return array 660 | */ 661 | public function toOptionHash() 662 | { 663 | return $this->_toOptionHash('_id', 'name'); 664 | } 665 | 666 | /** 667 | * Before load action 668 | */ 669 | protected function _beforeLoad() { } 670 | 671 | /** 672 | * Process loaded collection data 673 | */ 674 | protected function _afterLoadData() { } 675 | 676 | /** 677 | * Load references 678 | */ 679 | protected function _loadReferences() 680 | { 681 | foreach($this->_preloadFields as $field => $true) 682 | { 683 | $collection = $this->getReferencedCollection($field); 684 | if( ! $collection->isLoaded()) { 685 | $this->applyReferencedCollectionFilter($field, $collection); 686 | $collection->addToCache(TRUE); 687 | } 688 | } 689 | } 690 | 691 | /** 692 | * After load action 693 | */ 694 | protected function _afterLoad() 695 | { 696 | if(isset($this->_addToCacheAfterLoad)) { 697 | $this->addToCache(); 698 | } 699 | } 700 | 701 | /** 702 | * Build query condition. See docs for addFieldToFilter. 703 | * 704 | * @param string|array $fieldName 705 | * @param integer|string|array $condition 706 | * @param mixed $_condition 707 | * @return array 708 | */ 709 | protected function _getCondition($fieldName, $condition = NULL, $_condition = NULL) 710 | { 711 | // If $fieldName is an array it is assumed to be multiple queries as $fieldName => $condition 712 | if (is_array($fieldName)) { 713 | $query = array(); 714 | foreach($fieldName as $_fieldName => $_condition) { 715 | $query = array_merge($query, $this->_getCondition($_fieldName, $_condition)); 716 | } 717 | } 718 | 719 | // When using third argument, convert to two arguments 720 | else if ( func_num_args() == 3 ) { 721 | $query = $this->_getCondition($fieldName, array($condition => $_condition)); 722 | } 723 | 724 | // Handle multi-field filters with field names like firstname|lastname using $or 725 | else if (strpos($fieldName, '|')) { 726 | $query = array(); 727 | foreach(explode('|', $fieldName) as $_fieldName) { 728 | switch (func_num_args()) { 729 | case 2: 730 | $query[] = $this->_getCondition($_fieldName, $condition); break; 731 | case 3: 732 | default: 733 | $query[] = $this->_getCondition($_fieldName, $condition, $_condition); break; 734 | } 735 | } 736 | $query = array('$or' => $query); 737 | } 738 | 739 | // Handle cross-collection filters with field names like bar_id->name 740 | else if (strpos($fieldName, '->')) { 741 | list($reference,$referenceField) = explode('->', $fieldName, 2); 742 | $collection = Mage::getSingleton($this->getResource()->getFieldModelName($reference))->getCollection(); 743 | switch (func_num_args()) { 744 | case 2: 745 | $collection->addFieldToFilter($referenceField, $condition); break; 746 | case 3: 747 | default: 748 | $collection->addFieldToFilter($referenceField, $condition, $_condition); break; 749 | } 750 | $query = array($reference => array('$in' => $collection->getAllIds(TRUE))); 751 | } 752 | 753 | // Process sub-queries of or and nor 754 | else if ($fieldName == 'or' || $fieldName == 'nor') { 755 | $query = array(); 756 | foreach($condition as $_condition) { 757 | $query[] = $this->_getCondition($_condition); 758 | } 759 | $query = array('$or' => $query); 760 | } 761 | 762 | // Process sub-queries of $and operator 763 | else if ($fieldName == 'and') { 764 | $query = array(); 765 | foreach($condition as $_condition) { 766 | $query[] = $this->_getCondition($_condition); 767 | } 768 | $query = array('$and' => $query); 769 | } 770 | 771 | // Process where (javascript) queries 772 | else if ($fieldName == 'where') { 773 | $query = array('$where' => $condition); 774 | } 775 | 776 | // Bypass all processing if field name is an operator 777 | else if (substr($fieldName,0,1) == '$') { 778 | $query = array($fieldName => $condition); 779 | } 780 | 781 | // Process special condition keys 782 | else if (is_array($condition)) { 783 | 784 | // If condition key is an operator, make no changes 785 | if (count($condition) == 1 && substr(key($condition),0,1) == '$') { 786 | $query = array($fieldName => $condition); 787 | } 788 | 789 | // Range queries 790 | elseif (isset($condition['from']) || isset($condition['to'])) { 791 | $query = array(); 792 | if (isset($condition['from'])) { 793 | $query['$gte'] = $this->castFieldValue($fieldName, $condition['from']); 794 | } 795 | if (isset($condition['to'])) { 796 | $query['$lte'] = $this->castFieldValue($fieldName, $condition['to']); 797 | } 798 | $query = array($fieldName => $query); 799 | } 800 | 801 | // Multiple conditions 802 | elseif (count($condition) > 1) { 803 | $query = array(); 804 | foreach($condition as $_condition => $value) { 805 | $_condition = $this->_getCondition($fieldName, array($_condition => $value)); 806 | $query = array_merge($query, $_condition[$fieldName]); 807 | } 808 | $query = array($fieldName => $query); 809 | } 810 | 811 | // Equality 812 | elseif (array_key_exists('eq', $condition)) { 813 | // Nested data, assume exact match (should we bother to typecast?) 814 | if(strpos($fieldName,'.') !== false) { 815 | $query = array($fieldName => $condition['eq']); 816 | } 817 | // Search array for presence of a single value 818 | else if( 819 | ! is_array($condition['eq']) && 820 | in_array($this->getResource()->getFieldType($fieldName), array('set','referenceSet','embeddedSet')) 821 | ) { 822 | $query = array($fieldName => $condition['eq']); 823 | } 824 | // Search for an exact match 825 | else { 826 | $query = array($fieldName => $this->castFieldValue($fieldName, $condition['eq'])); 827 | } 828 | } 829 | elseif (array_key_exists('neq', $condition)) { 830 | // Search array for presence of a single value 831 | if( ! is_array($condition['neq']) && $this->getResource()->getFieldType($fieldName) == 'set') { 832 | $query = array($fieldName => array('$ne' => $condition['neq'])); 833 | } 834 | // Search for an exact match 835 | else { 836 | $query = array($fieldName => array('$ne' => $this->castFieldValue($fieldName, $condition['neq']))); 837 | } 838 | } 839 | 840 | // Like (convert SQL like to regex) 841 | elseif (array_key_exists('like', $condition)) { 842 | $query = preg_quote($condition['like']); 843 | $query = str_replace('\_', '_', $query); // unescape SQL syntax 844 | if(strlen($query) && $query{0} != '%') { 845 | $query = '^'.$query; 846 | } 847 | if(strlen($query) && substr($query,-1) != '%') { 848 | $query = $query.'$'; 849 | } 850 | $query = trim($query,'%'); 851 | $query = array($fieldName => new MongoRegex('/'.str_replace('%','.*',$query).'/i')); 852 | } 853 | elseif (array_key_exists('nlike', $condition)) { 854 | $query = preg_quote($condition['nlike']); 855 | $query = str_replace('\_', '_', $query); // unescape SQL syntax 856 | if(strlen($query) && $query{0} != '%') { 857 | $query = '^'.$query; 858 | } 859 | if(strlen($query) && substr($query,-1) != '%') { 860 | $query = $query.'$'; 861 | } 862 | $query = trim($query,'%'); 863 | $query = array($fieldName => array('$not' => new MongoRegex('/'.str_replace('%','.*',$query).'/i'))); 864 | } 865 | 866 | // Test null by type 867 | elseif (array_key_exists('notnull', $condition)) { 868 | $query = array($fieldName => array('$not' => array('$type' => Mongo_Database::TYPE_NULL))); 869 | } 870 | elseif (array_key_exists('null', $condition)) { 871 | $query = array($fieldName => array('$type' => Mongo_Database::TYPE_NULL)); 872 | } 873 | elseif (array_key_exists('is', $condition)) { 874 | $query = strtoupper($condition['is']); 875 | if($query == 'NULL' || $query === NULL) { 876 | $query = array($fieldName => array('$type' => Mongo_Database::TYPE_NULL)); 877 | } else if($query == 'NOT NULL') { 878 | $query = array($fieldName => array('$not' => array('$type' => Mongo_Database::TYPE_NULL))); 879 | } 880 | } 881 | 882 | // Array queries 883 | elseif (array_key_exists('in', $condition)) { 884 | $values = array(); 885 | if ( ! empty($condition['in'])) { 886 | if (in_array($this->getResource()->getFieldType($fieldName), array('set','referenceSet','embeddedSet','enumSet'))) { 887 | $values = $this->castFieldValue($fieldName, $condition['in']); 888 | } 889 | else { 890 | foreach($condition['in'] as $value) { 891 | $values[] = $this->castFieldValue($fieldName, $value); 892 | } 893 | } 894 | } 895 | $query = array($fieldName => array('$in' => $values)); 896 | } 897 | elseif (array_key_exists('nin', $condition)) { 898 | $values = array(); 899 | if ( ! empty($condition['nin'])) { 900 | if (in_array($this->getResource()->getFieldType($fieldName), array('set','referenceSet','embeddedSet','enumSet'))) { 901 | $values = $this->castFieldValue($fieldName, $condition['nin']); 902 | } 903 | else { 904 | foreach($condition['nin'] as $value) { 905 | $values[] = $this->castFieldValue($fieldName, $value); 906 | } 907 | } 908 | } 909 | $query = array($fieldName => array('$nin' => $values)); 910 | } 911 | 912 | // Bypass typecasting 913 | elseif (array_key_exists('raw', $condition)) { 914 | $query = array($fieldName => $condition['raw']); 915 | } 916 | 917 | // Assume an "equals" query 918 | else { 919 | $query = $this->_getCondition($fieldName, array('eq' => $condition)); 920 | } 921 | } 922 | 923 | // Condition is scalar 924 | else { 925 | $query = $this->_getCondition($fieldName, array('eq' => $condition)); 926 | } 927 | 928 | return $query; 929 | } 930 | 931 | /** 932 | * Add sort order to the end or to the beginning 933 | * 934 | * @param string|array $field 935 | * @param string $direction 936 | * @param bool $unshift 937 | * @return Cm_Mongo_Model_Resource_Collection_Abstract 938 | */ 939 | protected function _setOrder($field, $direction, $unshift = false) 940 | { 941 | if(is_array($field)) { 942 | foreach($field as $key => $value) { 943 | if(is_int($key)) { 944 | $_field = $value; 945 | $_direction = $direction; 946 | } 947 | else { 948 | $_field = $key; 949 | $_direction = $value; 950 | } 951 | $this->_setOrder($_field, $_direction, $unshift); 952 | } 953 | return $this; 954 | } 955 | 956 | if( ! ctype_digit($direction)) { 957 | $direction = (strtoupper($direction) == self::SORT_ORDER_ASC) ? Mongo_Collection::ASC : Mongo_Collection::DESC; 958 | } 959 | 960 | // emulate associative unshift 961 | if ($unshift) { 962 | $orders = array($field => $direction); 963 | foreach ((array)$this->_query->get_option('sort') as $key => $_direction) { 964 | if (!isset($orders[$key])) { 965 | $orders[$key] = $_direction; 966 | } 967 | } 968 | $this->_query->set_option('sort', $orders); 969 | } 970 | else { 971 | $this->_query->sort($field, $direction); 972 | } 973 | return $this; 974 | } 975 | 976 | /** 977 | * Apply the limit and skip based on paging variables 978 | * @return \Cm_Mongo_Model_Resource_Collection_Abstract 979 | */ 980 | protected function _renderLimit() 981 | { 982 | if($this->_pageSize){ 983 | if($this->getCurPage() > 1) { 984 | $this->_query->skip(($this->getCurPage()-1) * $this->_pageSize); 985 | } 986 | $this->_query->limit($this->_pageSize); 987 | } 988 | return $this; 989 | } 990 | 991 | /** 992 | * Make sure that preloaded fields are included in query 993 | */ 994 | protected function _renderFields() 995 | { 996 | $fields = $this->_query->get_option('fields'); 997 | if($fields && array_sum($fields)) { 998 | foreach($this->_preloadFields as $field => $true) { 999 | $fields[$field] = 1; 1000 | } 1001 | $this->_query->set_option('fields', $fields); 1002 | } 1003 | } 1004 | 1005 | } 1006 | -------------------------------------------------------------------------------- /code/Model/Resource/Collection/Embedded.php: -------------------------------------------------------------------------------- 1 | _isReplaced = $flag; 25 | } 26 | return $this->_isReplaced; 27 | } 28 | 29 | /** 30 | * Store the root object and path so that items added can have proper root and path 31 | * 32 | * @param Cm_Mongo_Model_Abstract $object 33 | * @param string $path 34 | */ 35 | public function _setEmbeddedIn($object, $path) 36 | { 37 | $this->_root = $object; 38 | $this->_path = $path; 39 | } 40 | 41 | /** 42 | * Override addItem to not attempt to use item id as key, and to set item root and path. 43 | * Also to ensure indexing on strings for safety with MongoId 44 | * 45 | * @param Varien_Object $item 46 | * @return Cm_Mongo_Model_Resource_Collection_Embedded 47 | */ 48 | public function addItem(Varien_Object $item) 49 | { 50 | $index = count($this->_items); 51 | $item->_setEmbeddedIn($this->_root, $this->_path.'.'.$index); 52 | $itemId = $item->getIdFieldName() ? $this->_getItemKey($item->getId()) : NULL; 53 | 54 | if ($itemId !== NULL) { 55 | if (isset($this->_items[$itemId])) { 56 | throw new Exception('Item ('.get_class($item).') with the same id "'.$item->getId().'" already exist'); 57 | } 58 | $this->_items[$itemId] = $item; 59 | } else { 60 | $this->_items[] = $item; 61 | } 62 | return $this; 63 | } 64 | 65 | /** 66 | * Checks to see if any of the items have data changes. 67 | * Provides interface consistent with Cm_Mongo_Model_Abstract 68 | * 69 | * @return boolean 70 | */ 71 | public function hasDataChanges() 72 | { 73 | foreach($this->_items as $item) { 74 | if($item->hasDataChanges()) { 75 | return TRUE; 76 | } 77 | } 78 | return FALSE; 79 | } 80 | 81 | /** 82 | * Call the reset method of each item and clear the collection. 83 | * Provides interface consistent with Cm_Mongo_Model_Abstract 84 | * 85 | * @return Cm_Mongo_Model_Resource_Collection_Embedded 86 | */ 87 | public function reset() 88 | { 89 | foreach ($this->_items as $item) { 90 | $item->reset(); 91 | } 92 | $this->clear(); 93 | $this->_root = $this->_path = NULL; 94 | return $this; 95 | } 96 | 97 | } 98 | 99 | -------------------------------------------------------------------------------- /code/Model/Resource/Setup.php: -------------------------------------------------------------------------------- 1 | _mongoConn = $this->_conn; 20 | $this->_conn = Mage::getSingleton('core/resource')->getConnection('core_setup'); 21 | } 22 | 23 | /** 24 | * @return Mongo_Database 25 | */ 26 | public function getMongoDatabase() 27 | { 28 | return $this->_mongoConn; 29 | } 30 | 31 | /** 32 | * Run javascript through the mongo shell (uses temporary file and exec) 33 | * 34 | * @param string $js 35 | * @return Cm_Mongo_Model_Resource_Setup 36 | */ 37 | public function runJs($js) 38 | { 39 | $filename = tempnam(sys_get_temp_dir(), 'magento').'.js'; 40 | file_put_contents($filename, $js); 41 | $output = shell_exec("mongo --quiet {$this->_connectionConfig->server}/{$this->_connectionConfig->database} $filename"); 42 | unlink($filename); 43 | if($output) { 44 | throw new Exception($output); 45 | } 46 | return $this; 47 | } 48 | 49 | /** 50 | * Run module modification files. Return version of last applied upgrade (false if no upgrades applied) 51 | * 52 | * @param string $actionType install|upgrade|uninstall 53 | * @param string $fromVersion 54 | * @param string $toVersion 55 | * @return string|bool 56 | */ 57 | 58 | protected function _modifyResourceDb($actionType, $fromVersion, $toVersion) 59 | { 60 | $resModel = (string)$this->_connectionConfig->model; 61 | $modName = (string)$this->_moduleConfig[0]->getName(); 62 | 63 | $sqlFilesDir = Mage::getModuleDir('sql', $modName).DS.$this->_resourceName; 64 | if (!is_dir($sqlFilesDir) || !is_readable($sqlFilesDir)) { 65 | return false; 66 | } 67 | // Read resource files 68 | $arrAvailableFiles = array(); 69 | $sqlDir = dir($sqlFilesDir); 70 | while (false !== ($sqlFile = $sqlDir->read())) { 71 | $matches = array(); 72 | if (preg_match('#^'.$resModel.'-'.$actionType.'-(.*)\.(js|php)$#i', $sqlFile, $matches)) { 73 | $arrAvailableFiles[$matches[1]] = $sqlFile; 74 | } 75 | } 76 | $sqlDir->close(); 77 | if (empty($arrAvailableFiles)) { 78 | return false; 79 | } 80 | 81 | // Get SQL files name 82 | $arrModifyFiles = $this->_getModifySqlFiles($actionType, $fromVersion, $toVersion, $arrAvailableFiles); 83 | if (empty($arrModifyFiles)) { 84 | return false; 85 | } 86 | 87 | $modifyVersion = false; 88 | foreach ($arrModifyFiles as $resourceFile) { 89 | $sqlFile = $sqlFilesDir.DS.$resourceFile['fileName']; 90 | $fileType = pathinfo($resourceFile['fileName'], PATHINFO_EXTENSION); 91 | // Execute SQL 92 | if ($this->_conn) { 93 | if (method_exists($this->_conn, 'disallowDdlCache')) { 94 | $this->_conn->disallowDdlCache(); 95 | } 96 | try { 97 | switch ($fileType) { 98 | case 'js': 99 | $result = true; 100 | $output = shell_exec("mongo --quiet {$this->_connectionConfig->server}/{$this->_connectionConfig->database} $sqlFile"); 101 | if($output) { 102 | throw new Exception($output); 103 | } 104 | break; 105 | case 'php': 106 | $result = include($sqlFile); 107 | break; 108 | default: 109 | $result = false; 110 | } 111 | if ($result) { 112 | if (strpos($actionType, 'data-') !== false) { 113 | $this->_getResource()->setDataVersion($this->_resourceName, $resourceFile['toVersion']); 114 | } else { 115 | $this->_getResource()->setDbVersion($this->_resourceName, $resourceFile['toVersion']); 116 | } 117 | } 118 | } catch (Exception $e){ 119 | echo "
".print_r($e,1)."
"; 120 | throw Mage::exception('Mage_Core', Mage::helper('core')->__('Error in file: "%s" - %s', $sqlFile, $e->getMessage())); 121 | } 122 | if (method_exists($this->_conn, 'allowDdlCache')) { 123 | $this->_conn->allowDdlCache(); 124 | } 125 | } 126 | $modifyVersion = $resourceFile['toVersion']; 127 | } 128 | self::$_hadUpdates = true; 129 | return $modifyVersion; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /code/Model/Resource/Type/Mongo.php: -------------------------------------------------------------------------------- 1 | config, $config->asCanonicalArray()) 17 | : Cm_Mongo_Model_Resource_Type_Shim::instance($config['config'], $config); 18 | 19 | // Set profiler 20 | $conn->set_profiler(array($this, 'start_profiler'), array($this, 'stop_profiler')); 21 | 22 | return $conn; 23 | } 24 | 25 | /** 26 | * @param string $group 27 | * @param string $query 28 | * @return string 29 | */ 30 | public function start_profiler($group, $query) 31 | { 32 | $key = "$group::$query"; 33 | Cm_Mongo_Profiler::start($key); 34 | return $key; 35 | } 36 | 37 | /** 38 | * @param string $key 39 | */ 40 | public function stop_profiler($key) 41 | { 42 | Cm_Mongo_Profiler::stop($key); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /code/Model/Schema.php: -------------------------------------------------------------------------------- 1 | setCacheId(self::CACHE_KEY); 19 | $this->setCacheTags(array(self::CACHE_TAG)); 20 | parent::__construct($sourceData); 21 | 22 | if (Mage::app()->useCache(self::CACHE_KEY)) { 23 | $this->setCache(Mage::app()->getCache()); 24 | if ($this->loadCache()) { 25 | return; 26 | } 27 | } 28 | 29 | $config = Mage::getConfig()->loadModulesConfiguration('mongo.xml'); 30 | $this->setXml($config->getNode()); 31 | 32 | if (Mage::app()->useCache(self::CACHE_KEY)) { 33 | $this->saveCache(); 34 | } 35 | } 36 | 37 | /** 38 | * Get the collection name for a resource entity. 39 | * 40 | * @param string $resource 41 | * @param string $entity 42 | * @return string 43 | */ 44 | public function getCollectionName($resource, $entity = NULL) 45 | { 46 | return (string) $this->getEntitySchema($resource, $entity)->collection; 47 | } 48 | 49 | /** 50 | * Get the schema config node for a resource entity. 51 | * 52 | * @param string $resource 53 | * @param string $entity 54 | * @return Mage_Core_Model_Config_Element 55 | */ 56 | public function getEntitySchema($resource, $entity = NULL) 57 | { 58 | if(is_null($entity)) { 59 | list($resource, $entity) = explode('/',$resource); 60 | } 61 | return $this->getNode()->{$resource}->{$entity}; 62 | } 63 | 64 | /** 65 | * Get the field mappings for a resource entity. 66 | * 67 | * @param string $resource 68 | * @param string $entity 69 | * @return string 70 | */ 71 | public function getFieldMappings($resource, $entity = NULL) 72 | { 73 | return $this->getEntitySchema($resource, $entity)->fields->children(); 74 | } 75 | 76 | /** 77 | * Get the schema config node for a resource. 78 | * 79 | * @param string $resource 80 | * @return Varien_Simplexml_Element 81 | */ 82 | public function getResourceSchema($resource) 83 | { 84 | return $this->getNode()->{$resource}; 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /code/Model/Type/Interface.php: -------------------------------------------------------------------------------- 1 | precision) { 26 | if($mapping->mode) { 27 | return round( (float) $value, (int) $mapping->precision, (int) $mapping->mode); 28 | } else { 29 | return round( (float) $value, (int) $mapping->precision); 30 | } 31 | } 32 | return (float) $value; 33 | } 34 | 35 | public function bool($mapping, $value) 36 | { 37 | return (bool) $value; 38 | } 39 | 40 | public function MongoId($mapping, $value) 41 | { 42 | if($value instanceof MongoId) { 43 | return $value; 44 | } 45 | if(is_string($value)) { 46 | return new MongoId($value); 47 | } 48 | return NULL; 49 | } 50 | 51 | public function MongoDate($mapping, $value) 52 | { 53 | if($value instanceof MongoDate) { 54 | return $value; 55 | } 56 | else if(is_array($value) && isset($value['sec'])) { 57 | return new MongoDate($value['sec'], isset($value['usec']) ? $value['usec'] : 0); 58 | } 59 | 60 | $value = $this->timestamp($mapping, $value); 61 | if($value === NULL) { 62 | return NULL; 63 | } 64 | return new MongoDate((int)$value); 65 | } 66 | 67 | public function timestamp($mapping, $value) 68 | { 69 | if($value instanceof MongoDate) { 70 | return $value->sec; 71 | } 72 | else if($value === NULL) { 73 | return NULL; 74 | } 75 | else if(is_int($value) || is_float($value)) { 76 | return (int) $value; 77 | } 78 | else if($value instanceof Zend_Date) { 79 | return $value->getTimestamp(); 80 | } 81 | else if(is_array($value) && isset($value['sec'])) { 82 | return $value['sec']; 83 | } 84 | else if( ! strlen($value)) { 85 | return NULL; 86 | } 87 | else if(ctype_digit($value)) { 88 | return intval($value); 89 | } 90 | else if(($time = strtotime($value)) !== false) { 91 | return $time; 92 | } 93 | return NULL; 94 | } 95 | 96 | public function datestring($mapping, $value) 97 | { 98 | if(is_string($value)) { 99 | return $value; 100 | } 101 | else if($value instanceof Zend_Date) { 102 | return $value->toString(Varien_Date::DATE_INTERNAL_FORMAT); 103 | } 104 | else if(is_int($value)) { 105 | $date = new Zend_Date($value); 106 | return $date->toString(Varien_Date::DATE_INTERNAL_FORMAT); 107 | } 108 | else if($value instanceof MongoDate) { 109 | $date = new Zend_Date($value->sec); 110 | return $date->toString(Varien_Date::DATE_INTERNAL_FORMAT); 111 | } 112 | else { 113 | return (string) $value; 114 | } 115 | } 116 | 117 | public function set($mapping, $value) 118 | { 119 | if(is_string($value) && isset($mapping->split)) { 120 | $regex = ($mapping->split == 'newline' ? '/[ \t]*[\r\n]+[ \t]*/' : (string) $mapping->split); 121 | $value = preg_split($regex, trim($value), null, PREG_SPLIT_NO_EMPTY); 122 | } else if( ! is_array($value) || key($value) != 0) { 123 | $value = array_values((array) $value); 124 | } 125 | if($mapping->subtype) { 126 | $subtype = (string) $mapping->subtype; 127 | foreach($value as &$val) { 128 | $val = $this->$subtype($mapping, $val); 129 | } 130 | } 131 | return $value; 132 | } 133 | 134 | public function hash($mapping, $value) 135 | { 136 | if( ! count($value)) { 137 | return new ArrayObject; 138 | } 139 | if($mapping->subtype) { 140 | $subtype = (string) $mapping->subtype; 141 | foreach($value as $key => $val) { 142 | $value[$key] = $this->$subtype($mapping, $val); 143 | } 144 | } 145 | return (array) $value; 146 | } 147 | 148 | public function enum($mapping, $value) 149 | { 150 | return isset($mapping->options->$value) ? (string) $value : NULL; 151 | } 152 | 153 | public function enumSet($mapping, $value) 154 | { 155 | if($value instanceof Mage_Core_Model_Config_Element) { 156 | $value = array_keys($value->asCanonicalArray()); 157 | } 158 | 159 | if( ! is_array($value) || ! count($value)) { 160 | return array(); 161 | } 162 | $value = array_unique($value); 163 | $rejects = array(); 164 | foreach($value as $val) { 165 | if( ! $this->enum($mapping, $val)) { 166 | $rejects[] = $val; 167 | } 168 | } 169 | if($rejects) { 170 | return array_diff($value, $rejects); 171 | } 172 | return $value; 173 | } 174 | 175 | public function reference($mapping, $value) 176 | { 177 | $value = ($value instanceof Varien_Object ? $value->getId() : $value); 178 | return Mage::getResourceSingleton((string) $mapping->model)->castToMongo('_id', $value); 179 | } 180 | 181 | public function referenceSet($mapping, $value) 182 | { 183 | $ids = array(); 184 | foreach($value as $item) { 185 | $id = $item instanceof Varien_Object ? $item->getId() : $item; 186 | $ids[] = Mage::getResourceSingleton((string) $mapping->model)->castToMongo('_id', $id); 187 | } 188 | return $ids; 189 | } 190 | 191 | public function referenceHash($mapping, $value) 192 | { 193 | $items = array(); 194 | $idField = (string) $mapping->id_field; 195 | if ( ! $idField) { 196 | throw new Exception('Cannot cast value to referenceHash, no id_field defined.'); 197 | } 198 | foreach($value as $item) { 199 | $data = $item instanceof Varien_Object ? $item->getData() : $item; 200 | if ( ! empty($data[$idField])) { 201 | $data[$idField] = Mage::getResourceSingleton((string) $mapping->model)->castToMongo('_id', $data[$idField]); 202 | $items[] = $data; 203 | } 204 | } 205 | return $items; 206 | } 207 | 208 | public function __call($name, $args) 209 | { 210 | return Mage::getSingleton($name)->toMongo($args[0], $args[1]); 211 | } 212 | 213 | } 214 | -------------------------------------------------------------------------------- /code/Model/Type/Tophp.php: -------------------------------------------------------------------------------- 1 | timestamp($mapping, $value); 54 | if($value === NULL) { 55 | return NULL; 56 | } 57 | return new MongoDate((int)$value); 58 | } 59 | 60 | public function timestamp($mapping, $value) 61 | { 62 | if($value instanceof MongoDate) { 63 | return $value->sec; 64 | } 65 | else if($value === NULL) { 66 | return NULL; 67 | } 68 | else if(is_int($value) || is_float($value)) { 69 | return $value; 70 | } 71 | else if($value instanceof Zend_Date) { 72 | return $value->getTimestamp(); 73 | } 74 | else if(is_array($value) && isset($value['sec'])) { 75 | return $value['sec']; 76 | } 77 | else if( ! strlen($value)) { 78 | return NULL; 79 | } 80 | else if(ctype_digit($value)) { 81 | return intval($value); 82 | } 83 | else if(($time = strtotime($value)) !== false) { 84 | return $time; 85 | } 86 | return NULL; 87 | } 88 | 89 | public function datestring($mapping, $value) 90 | { 91 | if(is_string($value)) { 92 | return $value; 93 | } 94 | else if($value instanceof Zend_Date) { 95 | return $value->toString(Varien_Date::DATE_INTERNAL_FORMAT); 96 | } 97 | else if(is_int($value)) { 98 | $date = new Zend_Date($value); 99 | return $date->toString(Varien_Date::DATE_INTERNAL_FORMAT); 100 | } 101 | else if($value instanceof MongoDate) { 102 | $date = new Zend_Date($value->sec); 103 | return $date->toString(Varien_Date::DATE_INTERNAL_FORMAT); 104 | } 105 | else { 106 | return (string) $value; 107 | } 108 | } 109 | 110 | public function set($mapping, $value) 111 | { 112 | if(is_string($value) && isset($mapping->split)) { 113 | $regex = ($mapping->split == 'newline' ? '/[ \t]*[\r\n]+[ \t]*/' : (string) $mapping->split); 114 | $value = preg_split($regex, trim($value), null, PREG_SPLIT_NO_EMPTY); 115 | } else if( ! is_array($value) || key($value) != 0) { 116 | $value = array_values((array) $value); 117 | } 118 | if($mapping->subtype) { 119 | $subtype = (string) $mapping->subtype; 120 | foreach($value as &$val) { 121 | $val = $this->$subtype($mapping, $val); 122 | } 123 | } 124 | return $value; 125 | } 126 | 127 | public function hash($mapping, $value) 128 | { 129 | return (array) $value; 130 | } 131 | 132 | public function enum($mapping, $value) 133 | { 134 | return isset($mapping->options->$value) ? (string) $value : NULL; 135 | } 136 | 137 | public function enumSet($mapping, $value) 138 | { 139 | if( ! is_array($value) || ! count($value)) { 140 | return array(); 141 | } 142 | $value = array_unique($value); 143 | $rejects = array(); 144 | foreach($value as $val) { 145 | if( ! $this->enum($mapping, $val)) { 146 | $rejects[] = $val; 147 | } 148 | } 149 | if($rejects) { 150 | return array_diff($value, $rejects); 151 | } 152 | return $value; 153 | } 154 | 155 | public function reference($mapping, $value) 156 | { 157 | return Mage::getResourceSingleton((string) $mapping->model)->castToMongo('_id', $value); 158 | } 159 | 160 | public function referenceSet($mapping, $value) 161 | { 162 | $resource = Mage::getResourceSingleton((string) $mapping->model); 163 | $ids = array(); 164 | foreach($value as $id) { 165 | $ids[] = $resource->castToMongo('_id', $id); 166 | } 167 | return $ids; 168 | } 169 | 170 | public function referenceHash($mapping, $value) 171 | { 172 | return (array) $value; 173 | } 174 | 175 | public function __call($name, $args) 176 | { 177 | return Mage::getSingleton($name)->toPHP($args[0], $args[1]); 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /code/Profiler.php: -------------------------------------------------------------------------------- 1 | false, 24 | 'count'=>0, 25 | 'sum'=>0, 26 | ); 27 | } 28 | 29 | /** 30 | * @static 31 | * @ignore 32 | * @param string $timerName 33 | */ 34 | public static function resume($timerName) 35 | { 36 | if (empty(self::$_timers[$timerName])) { 37 | self::reset($timerName); 38 | } 39 | self::$_timers[$timerName]['start'] = microtime(true); 40 | self::$_timers[$timerName]['count'] ++; 41 | } 42 | 43 | /** 44 | * @static 45 | * @ignore 46 | * @param string $timerName 47 | */ 48 | public static function start($timerName) 49 | { 50 | self::resume($timerName); 51 | } 52 | 53 | /** 54 | * @static 55 | * @ignore 56 | * @param string $timerName 57 | */ 58 | public static function pause($timerName) 59 | { 60 | if (empty(self::$_timers[$timerName])) { 61 | self::reset($timerName); 62 | } 63 | if (false!==self::$_timers[$timerName]['start']) { 64 | self::$_timers[$timerName]['sum'] += microtime(true)-self::$_timers[$timerName]['start']; 65 | self::$_timers[$timerName]['start'] = false; 66 | } 67 | } 68 | 69 | /** 70 | * @static 71 | * @ignore 72 | * @param string $timerName 73 | */ 74 | public static function stop($timerName) 75 | { 76 | self::pause($timerName); 77 | } 78 | 79 | /** 80 | * @static 81 | * @param string $timerName 82 | * @param string $key 83 | * @return bool|mixed 84 | */ 85 | public static function fetch($timerName, $key='sum') 86 | { 87 | if (empty(self::$_timers[$timerName])) { 88 | return false; 89 | } elseif (empty($key)) { 90 | return self::$_timers[$timerName]; 91 | } 92 | switch ($key) { 93 | case 'sum': 94 | $sum = self::$_timers[$timerName]['sum']; 95 | if (self::$_timers[$timerName]['start']!==false) { 96 | $sum += microtime(true)-self::$_timers[$timerName]['start']; 97 | } 98 | return $sum; 99 | 100 | case 'count': 101 | $count = self::$_timers[$timerName]['count']; 102 | return $count; 103 | 104 | default: 105 | if (!empty(self::$_timers[$timerName][$key])) { 106 | return self::$_timers[$timerName][$key]; 107 | } 108 | } 109 | return false; 110 | } 111 | 112 | /** 113 | * @static 114 | * @return array 115 | */ 116 | public static function getTimers() 117 | { 118 | return self::$_timers; 119 | } 120 | 121 | /** 122 | * Print (or get) all timers 123 | * 124 | * @static 125 | * @param bool $return 126 | * @return string 127 | */ 128 | public static function debug($return = false) 129 | { 130 | if($return) { 131 | ob_start(); 132 | } 133 | foreach(self::$_timers as $name => $timer) { 134 | printf("%d (%1.4fms): %s\n", $timer['count'], $timer['sum'], $name); 135 | } 136 | if($return) { 137 | return ob_get_clean(); 138 | } 139 | return NULL; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /code/controllers/Adminhtml/EnumController.php: -------------------------------------------------------------------------------- 1 | loadLayout() 8 | ->_setActiveMenu('system/enums') 9 | ->_title($this->__('Enumerations')) 10 | ->_addContent($this->getLayout()->createBlock('mongo/adminhtml_enum')) 11 | ->renderLayout(); 12 | } 13 | 14 | public function newAction() 15 | { 16 | $this->_forward('edit'); 17 | } 18 | 19 | public function editAction() 20 | { 21 | $id = $this->getRequest()->getParam('id'); 22 | $model = Mage::getModel('mongo/enum'); 23 | Mage::register('edit_model', $model); 24 | 25 | if( $id ){ 26 | $model->load($id); 27 | if( ! $model->getId()) { 28 | $this->_getSession()->addError($this->__('This enumeration no longer exists.')); 29 | $this->_redirect('*/*/'); 30 | return; 31 | } 32 | } 33 | 34 | $data = $this->_getSession()->getFormData(true); 35 | if( ! empty($data)) { 36 | $model->loadData($data); 37 | } 38 | 39 | $this->loadLayout() 40 | ->_setActiveMenu('system/enums') 41 | ->_title($this->__('Enumerations')) 42 | ->_title($model->getId() ? $this->__('Edit %s', $model->getName()) : $this->__('New Enumeration')) 43 | ->_addContent($this->getLayout()->createBlock('mongo/adminhtml_enum_edit')) 44 | ->renderLayout(); 45 | } 46 | 47 | public function saveAction() 48 | { 49 | if($data = $this->getRequest()->getPost()) { 50 | $id = $data['_id']; 51 | $model = Mage::getModel('mongo/enum'); 52 | /* @var Cm_Mongo_Model_Enum $model */ 53 | 54 | if( ! $id ) { 55 | $this->_getSession()->addError($this->__('Enumeration identifier not specified.')); 56 | $this->_getSession()->setFormData(FALSE); 57 | $this->_redirect('*/*/'); 58 | return; 59 | } 60 | 61 | $model->load($id); 62 | 63 | try { 64 | 65 | $defaults = array(); 66 | unset($data['defaults']['__empty']); 67 | foreach($data['defaults'] as $key => $value) { 68 | $index = $value['value']; 69 | if( ! strlen($index)) { 70 | continue; 71 | } 72 | unset($value['value']); 73 | $defaults[$index] = $value; 74 | } 75 | $data['defaults'] = $defaults; 76 | 77 | // Load data 78 | $model->setData($data); 79 | 80 | $model->save(); 81 | 82 | $this->_getSession()->addSuccess($this->__('%s has been saved.', $model->getName())); 83 | $this->_getSession()->setFormData(FALSE); 84 | if($this->getRequest()->getParam('back')) { 85 | $this->_redirect('*/*/edit', array('id' => $model->getId())); 86 | return; 87 | } 88 | } 89 | catch(Exception $e) { 90 | $this->_getSession()->addError($e->getMessage()); 91 | $this->_getSession()->setFormData(FALSE); 92 | $this->_redirect('*/*/edit', array('id' => $id)); 93 | return; 94 | } 95 | } 96 | // back to grid by default 97 | $this->_redirect('*/*/'); 98 | } 99 | 100 | public function deleteAction() 101 | { 102 | $id = $this->getRequest()->getParam('id'); 103 | $model = Mage::getModel('mongo/enum'); 104 | 105 | $model->load($id); 106 | if( ! $model->getId()) { 107 | $this->_getSession()->addError($this->__('This enumeration no longer exists.')); 108 | $this->_redirect('*/*/'); 109 | return; 110 | } 111 | 112 | $name = $model->getName(); 113 | $model->delete(); 114 | 115 | $this->_getSession()->addSuccess($this->__('%s has been deleted.', $name)); 116 | $this->_getSession()->setFormData(FALSE); 117 | $this->_redirect('*/*/'); 118 | } 119 | 120 | } -------------------------------------------------------------------------------- /code/etc/adminhtml.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Enumerations 8 | adminhtml/enum 9 | 105 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | Manage Enumerations 22 | 105 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /code/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 0.1.0 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Cm_Mongo 16 | Cm_Mongo_Model_Resource_Setup 17 | 18 | 19 | mongo 20 | default 21 | localhost 22 | magento 23 | mongo 24 | 1 25 | 26 | 27 | 28 | 29 | mongo_setup 30 | 31 | 32 | 33 | 34 | mongo_setup 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Cm_Mongo_Model_Resource_Type_Mongo 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | Mongo Schema description files (mongo.xml). 54 | MONGO_SCHEMA 55 | 56 | 57 | 58 | 59 | 60 | 61 | Cm_Mongo_Block 62 | 63 | 64 | 65 | 66 | 67 | Cm_Mongo_Helper 68 | 69 | 70 | 71 | 72 | 73 | Cm_Mongo_Model 74 | mongo_mongo 75 | 76 | 77 | Cm_Mongo_Model_Mongo 78 | 79 | 80 | 81 | 82 | 83 | 84 | mongo/indexer 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | Cm_Mongo_Adminhtml 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | mongo.xml 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | mongo.xml 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | mongo/job::runCron 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 0 140 | 50 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | mongo/fixture 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /code/etc/mongo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | enums 8 | 9 | <_id/> 10 | 11 | hash1 12 | hash 13 | 14 | 15 | 16 | 17 | 18 | job_queue 19 | 20 | 21 | 22 | <_id> MongoId 23 | 24 | hash 25 | 26 | 27 | int 28 | int 29 | int 30 | MongoDate 31 | set 32 | hash 33 | MongoDate 34 | MongoDate 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /code/etc/mongo.xml.sample: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | foobar 12 | 13 | 14 | 15 | 16 | <_id> int 17 | referencefoo/baz 18 |
embeddedfoo/address
19 | 20 | mongo/type_encrypted 21 | enum 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | bool 30 | setstring 31 | MongoDate 32 | MongoDate 33 |
34 |
35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | setfloat 44 | 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /code/etc/mongo_indexer.xml.sample: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | foo/bar 12 | foo/baz 13 | bar_id 14 | getName 15 | bar_name 16 | 17 | 18 | 19 | status 20 | active 21 | eq 22 | string 23 | 24 | 25 | 26 | relation_id->ban_id 27 | 28 | 29 | firstname 30 | lastname 31 | 32 | 33 | 34 | 35 | 36 | 37 | name/foo_bar_name 38 | getSurname 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /code/etc/system.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | select 11 | adminhtml/system_config_source_yesno 12 | 11 13 | 1 14 | 1 15 | 1 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 100 26 | 1 27 | 28 | 29 | 30 | select 31 | adminhtml/system_config_source_yesno 32 | 10 33 | 1 34 | Run the Mongo Queue via the Magento Cron. It is recommended to run the Mongo Queue via a separate unix cron job. 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "colinmollenhour/magento-mongo", 3 | "type": "magento-module", 4 | "license":"BSD-3-Clause", 5 | "description":"MongoDb abstraction layer and atomic job queue for Magento.", 6 | "authors":[ 7 | { 8 | "name":"Colin Mollenhour", 9 | "email":"colin@mollenhour.com" 10 | } 11 | ], 12 | "require": { 13 | "magento-hackathon/magento-composer-installer": "*" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /layout/mongo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | code app/code/community/Cm/Mongo 2 | Cm_Mongo.xml app/etc/modules/Cm_Mongo.xml 3 | layout/mongo.xml app/design/adminhtml/default/default/layout/mongo.xml 4 | layout/mongo.xml app/design/frontend/base/default/layout/mongo.xml 5 | 6 | # Map file names to be autoload friendly 7 | lib/mongodb-php-odm/classes/json.php lib/JSON.php 8 | lib/mongodb-php-odm/classes/mongo/database.php lib/Mongo/Database.php 9 | lib/mongodb-php-odm/classes/mongo/collection.php lib/Mongo/Collection.php 10 | --------------------------------------------------------------------------------