├── .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 'Query | Time | Count |
';
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 | .''.$name.' | '
31 | .''.number_format($sum,4).' | '
32 | .''.$count.' | '
33 | .'
'
34 | ;
35 | }
36 | echo 'Total | '.number_format($allSum,4).' | '.$allCount.' |
';
37 | echo '
';
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 |
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 |
30 |
31 |
32 |
33 |
34 |
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 |
--------------------------------------------------------------------------------