123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722 |
- <?php
- class EMongoModel extends CModel
- {
- /**
- * @var EMongoClient the default database connection for all active record classes.
- * By default, this is the 'mongodb' application component.
- * @see getDbConnection
- */
- public static $db;
- private $_errors = array(); // attribute name => array of errors
- private $_attributes = array();
-
- private $_related = array();
- private $_partial = false;
- /**
- * @see yii/framework/CComponent::__get()
- * @param string $name
- * @return mixed
- */
- public function __get($name)
- {
- if(isset($this->_attributes[$name])){
- return $this->_attributes[$name];
- }
- if(isset($this->_related[$name])){
- return $this->_related[$name];
- }
- if(array_key_exists($name, $this->relations())){
- return $this->_related[$name] = $this->getRelated($name);
- }
- try{
- return parent::__get($name);
- }catch(CException $e){
- $getter = 'get' . $name;
- if(method_exists($this, $getter)){
- throw $e;
- }elseif(strncasecmp($name, 'on', 2) === 0 && method_exists($this, $name)){
- throw $e;
- }
- return null;
- }
- }
- /**
- * @see CComponent::__set()
- * @param string $name
- * @param mixed $value
- * @return mixed
- */
- public function __set($name, $value)
- {
- if(isset($this->_related[$name]) || array_key_exists($name, $this->relations())){
- return $this->_related[$name] = $value;
- }
- // This might be a little unperformant actually since Yiis own active record detects
- // If an attribute can be set first to ensure speed of accessing local variables...hmmm
- try{
- return parent::__set($name, $value);
- }catch(CException $e){
- return $this->setAttribute($name, $value);
- }
- }
- /**
- * @see CComponent::__isset()
- * @param string $name
- * @return bool
- */
- public function __isset($name)
- {
- if(isset($this->_attributes[$name])){
- return true;
- }
- if(isset($this->_related[$name])){
- return true;
- }
- if(array_key_exists($name, $this->relations())){
- return $this->getRelated($name) !== null;
- }
- return parent::__isset($name);
- }
- /**
- * @see CComponent::__unset()
- * @param string $name
- * @return void
- */
- public function __unset($name)
- {
- if(isset($this->_attributes[$name])){
- unset($this->_attributes[$name]);
- }elseif(isset($this->_related[$name])){
- unset($this->_related[$name]);
- }else{
- parent::__unset($name);
- }
- }
- /**
- * @see CComponent::__call()
- * @param string $name
- * @param array $parameters
- * @return mixed
- */
- public function __call($name,$parameters)
- {
- if(!array_key_exists($name, $this->relations())){
- return parent::__call($name,$parameters);
- }
- if(empty($parameters)){
- return $this->getRelated($name, false);
- }
- return $this->getRelated($name, false, $parameters[0]);
- }
- /**
- * This sets up our model.
- * Apart from what Yii normally does this also sets a field cache for reflection so that we only ever do reflection once to
- * understand what fields are in our model.
- * @param string $scenario
- */
- public function __construct($scenario = 'insert')
- {
- $this->getDbConnection()->setDocumentCache($this);
- if($scenario === null){ // internally used by populateRecord() and model()
- return;
- }
- $this->setScenario($scenario);
- $this->init();
- $this->attachBehaviors($this->behaviors());
- $this->afterConstruct();
- }
- /**
- * Initializes this model.
- * This method is invoked when an AR instance is newly created and has
- * its {@link scenario} set.
- * You may override this method to provide code that is needed to initialize the model (e.g. setting
- * initial property values.)
- * @return bool
- */
- public function init()
- {
- return true;
- }
- /**
- * @see CModel::attributeNames()
- * @return array
- */
- public function attributeNames()
- {
- $fields = $this->getDbConnection()->getFieldCache(get_class($this), true);
- $cols = array_merge($fields, array_keys($this->_attributes));
- return $cols !== null ? $cols : array();
- }
- /**
- * Holds all our relations
- * @return array
- */
- public function relations()
- {
- return array();
- }
- /**
- * Finds out if a document attributes actually exists
- * @param string $name
- * @return bool
- */
- public function hasAttribute($name)
- {
- $attrs = $this->_attributes;
- $fields = $this->getDbConnection()->getFieldCache(get_class($this));
- return isset($attrs[$name]) || isset($fields[$name]) || property_exists($this, $name) ? true : false;
- }
- /**
- * Sets the attribute of the model
- * @param string $name
- * @param mixed $value
- * @return bool
- */
- public function setAttribute($name, $value)
- {
- if(property_exists($this,$name)){
- $this->$name = $value;
- }else{
- $this->_attributes[$name] = $value;
- }
- return true;
- }
- /**
- * Gets a document attribute
- * @param string $name
- * @return mixed
- */
- public function getAttribute($name)
- {
- if(property_exists($this, $name)){
- return $this->$name;
- }
- if(isset($this->_attributes[$name])){
- return $this->_attributes[$name];
- }
- return null;
- }
- /**
- * @see CModel::getAttributes()
- * @param bool $names
- * @return array
- */
- public function getAttributes($names = true)
- {
- $attributes = $this->_attributes;
- $fields = $this->getDbConnection()->getFieldCache(get_class($this));
- if(is_array($fields)){
- foreach($fields as $name){
- $attributes[$name] = $this->$name;
- }
- }
- if(!is_array($names)){
- return $attributes;
- }
- $attrs = array();
- foreach($names as $name){
- if(property_exists($this, $name)){
- $attrs[$name] = $this->$name;
- }else{
- $attrs[$name] = isset($attributes[$name]) ? $attributes[$name] : null;
- }
- }
- return $attrs;
- }
- /**
- * Sets the attribute values in a massive way.
- * @param array $values attribute values (name=>value) to be set.
- * @param boolean $safeOnly whether the assignments should only be done to the safe attributes.
- * A safe attribute is one that is associated with a validation rule in the current {@link scenario}.
- * @see getSafeAttributeNames
- * @see attributeNames
- */
- public function setAttributes($values, $safeOnly = true)
- {
- if(!is_array($values)){
- return;
- }
- $attributes = array_flip($safeOnly ? $this->getSafeAttributeNames() : $this->attributeNames());
- $_meta = $this->getDbConnection()->getDocumentCache(get_class($this));
- foreach($values as $name => $value){
- $field_meta = isset($_meta[$name]) ? $_meta[$name] : array();
- if($safeOnly){
- if(isset($attributes[$name])){
- $this->$name = !is_bool($value) && !is_array($value) && !is_object($value) && preg_match('/^([0-9]|[1-9]{1}\d+)$/' /* Will only match real integers, unsigned */, $value) > 0
- && ( (PHP_INT_MAX > 2147483647 && (string)$value < '9223372036854775807') /* If it is a 64 bit system and the value is under the long max */
- || (string)$value < '2147483647' /* value is under 32bit limit */) ? (int)$value : $value;
- }elseif($safeOnly){
- $this->onUnsafeAttribute($name, $value);
- }
- }else{
- $this->$name = !is_bool($value) && !is_array($value) && !is_object($value) && preg_match('/^([0-9]|[1-9]{1}\d+)$$/' /* Will only match real integers, unsigned */, $value) > 0
- && ( (PHP_INT_MAX > 2147483647 && (string)$value < '9223372036854775807') || (string)$value < '2147483647') ? (int)$value : $value;
- }
- }
- }
- /**
- * Sets the attributes to be null.
- * @param array $names list of attributes to be set null. If this parameter is not given,
- * all attributes as specified by {@link attributeNames} will have their values unset.
- * @since 1.1.3
- */
- public function unsetAttributes($names = null)
- {
- if($names === null){
- $names = $this->attributeNames();
- }
- foreach($names as $name){
- $this->$name = null;
- }
- }
-
- /**
- * Allows for mass assignment of the record in question
- */
- public function populateRecord($attributes, $runEvent = true)
- {
- if($attributes === false || $attributes === null){
- return null;
- }
-
- $record = new $this;
- $record->setScenario('update');
-
- foreach($attributes as $name => $value){
- $record->setAttribute($name, $value);
- }
-
- $record->init();
- $record->attachBehaviors($record->behaviors());
- if($runEvent){
- $record->afterConstruct();
- }
- return $record;
- }
- /**
- * Sets whether or not this is a partial document
- * @param $partial
- */
- public function setIsPartial($partial)
- {
- $this->_partial = $partial;
- }
- /**
- * Gets whether or not this is a partial document, i.e. it only has some
- * of its fields present
- */
- public function getIsPartial()
- {
- return $this->_partial;
- }
- /**
- * You can change the primarykey but due to how MongoDB
- * actually works this IS NOT RECOMMENDED
- */
- public function primaryKey()
- {
- return '_id';
- }
- /**
- * Returns the related record(s).
- * This method will return the related record(s) of the current record.
- * If the relation is 'one' it will return a single object
- * or null if the object does not exist.
- * If the relation is 'many' it will return an array of objects
- * or an empty iterator.
- * @param string $name the relation name (see {@link relations})
- * @param boolean $refresh whether to reload the related objects from database. Defaults to false.
- * @param mixed $params array with additional parameters that customize the query conditions as specified in the relation declaration.
- * @return mixed the related object(s).
- * @throws EMongoException if the relation is not specified in {@link relations}.
- */
- public function getRelated($name, $refresh = false, $params = array())
- {
- if(!$refresh && $params === array() && (isset($this->_related[$name]) || array_key_exists($name, $this->_related))){
- return $this->_related[$name];
- }
- $relations = $this->relations();
- if(!isset($relations[$name])){
- throw new EMongoException(
- Yii::t(
- 'yii',
- '{class} does not have relation "{name}".',
- array('{class}' => get_class($this), '{name}' => $name)
- )
- );
- }
- Yii::trace('lazy loading ' . get_class($this) . '.' . $name, 'extensions.MongoYii.EMongoModel');
- $cursor = array();
- $relation = $relations[$name];
- // Let's get the parts of the relation to understand it entirety of its context
- $cname = $relation[1];
- $fkey = $relation[2];
- $pk = isset($relation['on']) ? $this->{$relation['on']} : $this->getPrimaryKey();
- $pkName = isset($relation['on']) ? $relation['on'] : $this->primaryKey();
- // This will detect . notation key names like AuthorName.id
- if(strpos($pkName, '.') !== false){
-
- $pk = array();
-
- $parts = explode('.', $pkName);
-
- if($this->hasAttribute($parts[0])){
-
- $val = $this->{$parts[0]};
-
- if(!is_array($val) && !is_object($val)){
- // continue
- }elseif(is_object($val) && property_exists($val, $parts[1])){
- $pk[] = $val->{$parts[1]};
- }elseif(is_array($val) && isset($val[$parts[1]])){
- $pk[] = $val[$parts[1]];
- }else{
- foreach($val as $k => $v){
- if(is_array($v) && isset($v[$parts[1]])){
- $pk[] = $v[$parts[1]];
- }elseif(is_object($v) && property_exists($v, $parts[1])){
- $pk[] = $v->{$parts[1]};
- }
- }
- }
- }
- }
-
- // This takes care of cases where the PK is an DBRef and only one DBRef, where it could
- // be mistaken as a multikey field
- if($relation[0] === 'one' && is_array($pk) && array_key_exists('$ref', $pk)){
- $pk = array($pk);
- }
-
- // Form the where clause
- $where = $params;
- if(isset($relation['where']) && !$params){
- $where = array_merge($relation['where'], $params);
- }
-
- // Find out what the pk is and what kind of condition I should apply to it
- if(is_array($pk)){
- //It is an array of references
- if(MongoDBRef::isRef(reset($pk))){
- $result = array();
- foreach($pk as $singleReference){
- $row = $this->populateReference($singleReference, $cname);
-
- // When $row does not exists it will return null. It will not add it to $result
- array_push($result, $row);
- }
-
- // When $row is null count($result) will be 0 and $result will be an empty array
- // Because we are a one relation we want to return null when a row does not exists
- // Currently it was returning an empty array
-
- if($relation[0] === 'one' && count($result) > 0){
- $result = $result[0];
- }
- return $this->_related[$name] = $result;
- }
- // It is an array of _ids
- $clause = array_merge($where, array($fkey => array('$in' => $pk)));
- }elseif($pk instanceof MongoDBRef){
- // I should probably just return it here
- // otherwise I will continue on
- return $this->_related[$name] = $this->populateReference($pk, $cname);
- }else{
- // It is just one _id
- $clause = array_merge($where, array($fkey => $pk));
- }
- $o = $cname::model($cname);
- if($relation[0] === 'one'){
- // Lets find it and return it
- return $this->_related[$name] = $o->findOne($clause);
- }elseif($relation[0] === 'many'){
- // Lets find them and return them
- $cursor = $o->find($clause)
- ->sort(isset($relation['sort']) ? $relation['sort'] : array())
- ->skip(isset($relation['skip']) ? $relation['skip'] : null)
- ->limit(isset($relation['limit']) ? $relation['limit'] : null);
-
- if(!isset($relation['cache']) || $relation['cache'] === true){
- return $this->_related[$name] = iterator_to_array($cursor);
- }
- }
- return $cursor; // FAIL SAFE
- }
- /**
- * @param mixed $reference Reference to populate
- * @param null|string $cname Class of model to populate. If not specified, populates data on current model
- * @return EMongoModel
- */
- public function populateReference($reference, $cname = null)
- {
- $row = MongoDBRef::get(self::$db->getDB(), $reference);
- $o = (is_null($cname)) ? $this : $cname::model();
- return $o->populateRecord($row);
- }
- /**
- * Returns a value indicating whether the named related object(s) has been loaded.
- * @param string $name the relation name
- * @return boolean a value indicating whether the named related object(s) has been loaded.
- */
- public function hasRelated($name)
- {
- return isset($this->_related[$name]) || array_key_exists($name, $this->_related);
- }
- /**
- * Sets the errors for that particular attribute
- * @param string $attribute
- * @param array $errors
- */
- public function setAttributeErrors($attribute, $errors)
- {
- $this->_errors[$attribute] = $errors;
- }
- /* THESE ERROR FUNCTIONS ARE ONLY HERE BECAUSE OF THE WAY IN WHICH PHP RESOLVES THE THE SCOPES OF VARS */
- // I needed to add the error handling function above but I had to include these as well
- /**
- * Returns a value indicating whether there is any validation error.
- * @param string $attribute attribute name. Use null to check all attributes.
- * @return boolean whether there is any error.
- */
- public function hasErrors($attribute = null)
- {
- if($attribute === null){
- return $this->_errors !== array();
- }
- return isset($this->_errors[$attribute]);
- }
- /**
- * Returns the errors for all attribute or a single attribute.
- * @param string $attribute attribute name. Use null to retrieve errors for all attributes.
- * @return array errors for all attributes or the specified attribute. Empty array is returned if no error.
- */
- public function getErrors($attribute = null)
- {
- if($attribute === null){
- return $this->_errors;
- }
- $attribute = trim(strtr($attribute, '][', '['), ']');
- if(strpos($attribute, '[') !== false){
- $prev = null;
- foreach(explode('[',$attribute) as $piece){
- if($prev === null && isset($this->errors[$piece])){
- $prev = $this->_errors[$piece];
- }elseif(isset($prev[$piece])){
- $prev = is_array($prev) ? $prev[$piece] : $prev->$piece;
- }
- }
- return $prev === null ? array() : $prev;
- }
- return isset($this->_errors[$attribute]) ? $this->_errors[$attribute] : array();
- }
- /**
- * Returns the first error of the specified attribute.
- * @param string $attribute attribute name.
- * @return string the error message. Null is returned if no error.
- */
- public function getError($attribute)
- {
- $attribute = trim(strtr($attribute, '][', '['), ']');
- if(strpos($attribute, '[') === false){
- return isset($this->_errors[$attribute]) ? reset($this->_errors[$attribute]) : null;
- }
- $prev = null;
- foreach(explode('[', $attribute) as $piece){
- if($prev === null && isset($this->_errors[$piece])){
- $prev = $this->_errors[$piece];
- }elseif(isset($prev[$piece])){
- $prev = is_array($prev) ? $prev[$piece] : $prev->$piece;
- }
- }
- return $prev === null ? null : reset($prev);
- }
- /**
- * Adds a new error to the specified attribute.
- * @param string $attribute attribute name
- * @param string $error new error message
- */
- public function addError($attribute,$error)
- {
- $this->_errors[$attribute][] = $error;
- }
- /**
- * Adds a list of errors.
- * @param array $errors a list of errors. The array keys must be attribute names.
- * The array values should be error messages. If an attribute has multiple errors,
- * these errors must be given in terms of an array.
- * You may use the result of {@link getErrors} as the value for this parameter.
- */
- public function addErrors($errors)
- {
- foreach($errors as $attribute => $error){
- if(is_array($error)){
- foreach($error as $e){
- $this->addError($attribute, $e);
- }
- }else{
- $this->addError($attribute, $error);
- }
- }
- }
- /**
- * Removes errors for all attributes or a single attribute.
- * @param string $attribute attribute name. Use null to remove errors for all attribute.
- */
- public function clearErrors($attribute = null)
- {
- if($attribute === null){
- $this->_errors = array();
- }else{
- unset($this->_errors[$attribute]);
- }
- }
- /**
- * Returns the database connection used by active record.
- * By default, the "mongodb" application component is used as the database connection.
- * You may override this method if you want to use a different database connection.
- * @return EMongoClient - the database connection used by active record.
- * @throws EMongoException
- */
- public function getDbConnection()
- {
- if(self::$db !== null){
- return self::$db;
- }
- self::$db = $this->getMongoComponent();
- if(self::$db instanceof EMongoClient){
- return self::$db;
- }
- throw new EMongoException(Yii::t('yii', 'MongoDB Active Record requires a "mongodb" EMongoClient application component.'));
- }
-
- /**
- * This allows you to define a custom override by default for models and the such
- */
- public function getMongoComponent()
- {
- return Yii::app()->mongodb;
- }
- /**
- * Cleans or rather resets the document
- * @return bool
- */
- public function clean()
- {
- $this->_attributes = array();
- $this->_related = array();
- // blank class properties
- $cache = $this->getDbConnection()->getDocumentCache(get_class($this));
- foreach($cache as $k => $v){
- $this->$k = null;
- }
- return true;
- }
- /**
- * Gets the formed document with MongoYii objects included
- * @return array
- */
- public function getDocument()
- {
- $attributes = $this->getDbConnection()->getFieldCache(get_class($this));
- $doc = array();
- if(is_array($attributes)){
- foreach($attributes as $field){
- $doc[$field] = $this->$field;
- }
- }
- return array_merge($doc, $this->_attributes);
- }
- /**
- * Gets the raw document with MongoYii objects taken out
- * @return array
- */
- public function getRawDocument()
- {
- return $this->filterRawDocument($this->getDocument());
- }
- /**
- * Filters a provided document to take out MongoYii objects.
- * @param array $doc
- * @return array
- */
- public function filterRawDocument($doc)
- {
- if(is_array($doc)){
- foreach($doc as $k => $v){
- if(is_array($v)){
- $doc[$k] = $this->{__FUNCTION__}($doc[$k]);
- }elseif($v instanceof EMongoModel || $v instanceof EMongoDocument){
- $doc[$k] = $doc[$k]->getRawDocument();
- }
- }
- }
- return $doc;
- }
- /**
- * Gets the JSON encoded document
- */
- public function getJSONDocument()
- {
- return json_encode($this->getRawDocument());
- }
- /**
- * Gets the BSON encoded document (never normally needed)
- */
- public function getBSONDocument()
- {
- return bson_encode($this->getRawDocument());
- }
- }
|