Page Menu
Home
c4science
Search
Configure Global Search
Log In
Files
F96446729
LiskDAO.php
No One
Temporary
Actions
Download File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Subscribers
None
File Metadata
Details
File Info
Storage
Attached
Created
Thu, Dec 26, 20:12
Size
50 KB
Mime Type
text/x-php
Expires
Sat, Dec 28, 20:12 (1 d, 8 h)
Engine
blob
Format
Raw Data
Handle
23184268
Attached To
rPH Phabricator
LiskDAO.php
View Options
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Simple object-authoritative data access object that makes it easy to build
* stuff that you need to save to a database. Basically, it means that the
* amount of boilerplate code (and, particularly, boilerplate SQL) you need
* to write is greatly reduced.
*
* Lisk makes it fairly easy to build something quickly and end up with
* reasonably high-quality code when you're done (e.g., getters and setters,
* objects, transactions, reasonably structured OO code). It's also very thin:
* you can break past it and use MySQL and other lower-level tools when you
* need to in those couple of cases where it doesn't handle your workflow
* gracefully.
*
* However, Lisk won't scale past one database and lacks many of the features
* of modern DAOs like Hibernate: for instance, it does not support joins or
* polymorphic storage.
*
* This means that Lisk is well-suited for tools like Differential, but often a
* poor choice elsewhere. And it is strictly unsuitable for many projects.
*
* Lisk's model is object-authoritative: the PHP class definition is the
* master authority for what the object looks like.
*
* =Building New Objects=
*
* To create new Lisk objects, extend @{class:LiskDAO} and implement
* @{method:establishLiveConnection}. It should return an
* @{class:AphrontDatabaseConnection}; this will tell Lisk where to save your
* objects.
*
* class Dog extends LiskDAO {
*
* protected $name;
* protected $breed;
*
* public function establishLiveConnection() {
* return $some_connection_object;
* }
* }
*
* Now, you should create your table:
*
* lang=sql
* CREATE TABLE dog (
* id int unsigned not null auto_increment primary key,
* name varchar(32) not null,
* breed varchar(32) not null,
* dateCreated int unsigned not null,
* dateModified int unsigned not null
* );
*
* For each property in your class, add a column with the same name to the table
* (see @{method:getConfiguration} for information about changing this mapping).
* Additionally, you should create the three columns `id`, `dateCreated` and
* `dateModified`. Lisk will automatically manage these, using them to implement
* autoincrement IDs and timestamps. If you do not want to use these features,
* see @{method:getConfiguration} for information on disabling them. At a bare
* minimum, you must normally have an `id` column which is a primary or unique
* key with a numeric type, although you can change its name by overriding
* @{method:getIDKey} or disable it entirely by overriding @{method:getIDKey} to
* return null. Note that many methods rely on a single-part primary key and
* will no longer work (they will throw) if you disable it.
*
* As you add more properties to your class in the future, remember to add them
* to the database table as well.
*
* Lisk will now automatically handle these operations: getting and setting
* properties, saving objects, loading individual objects, loading groups
* of objects, updating objects, managing IDs, updating timestamps whenever
* an object is created or modified, and some additional specialized
* operations.
*
* = Creating, Retrieving, Updating, and Deleting =
*
* To create and persist a Lisk object, use @{method:save}:
*
* $dog = id(new Dog())
* ->setName('Sawyer')
* ->setBreed('Pug')
* ->save();
*
* Note that **Lisk automatically builds getters and setters for all of your
* object's protected properties** via @{method:__call}. If you want to add
* custom behavior to your getters or setters, you can do so by overriding the
* @{method:readField} and @{method:writeField} methods.
*
* Calling @{method:save} will persist the object to the database. After calling
* @{method:save}, you can call @{method:getID} to retrieve the object's ID.
*
* To load objects by ID, use the @{method:load} method:
*
* $dog = id(new Dog())->load($id);
*
* This will load the Dog record with ID $id into $dog, or ##null## if no such
* record exists (@{method:load} is an instance method rather than a static
* method because PHP does not support late static binding, at least until PHP
* 5.3).
*
* To update an object, change its properties and save it:
*
* $dog->setBreed('Lab')->save();
*
* To delete an object, call @{method:delete}:
*
* $dog->delete();
*
* That's Lisk CRUD in a nutshell.
*
* = Queries =
*
* Often, you want to load a bunch of objects, or execute a more specialized
* query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this:
*
* $pugs = $dog->loadAllWhere('breed = %s', 'Pug');
* $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer');
*
* These methods work like @{function:queryfx}, but only take half of a query
* (the part after the WHERE keyword). Lisk will handle the connection, columns,
* and object construction; you are responsible for the rest of it.
* @{method:loadAllWhere} returns a list of objects, while
* @{method:loadOneWhere} returns a single object (or `null`).
*
* There's also a @{method:loadRelatives} method which helps to prevent the 1+N
* queries problem.
*
* = Managing Transactions =
*
* Lisk uses a transaction stack, so code does not generally need to be aware
* of the transactional state of objects to implement correct transaction
* semantics:
*
* $obj->openTransaction();
* $obj->save();
* $other->save();
* // ...
* $other->openTransaction();
* $other->save();
* $another->save();
* if ($some_condition) {
* $other->saveTransaction();
* } else {
* $other->killTransaction();
* }
* // ...
* $obj->saveTransaction();
*
* Assuming ##$obj##, ##$other## and ##$another## live on the same database,
* this code will work correctly by establishing savepoints.
*
* Selects whose data are used later in the transaction should be included in
* @{method:beginReadLocking} or @{method:beginWriteLocking} block.
*
* @task conn Managing Connections
* @task config Configuring Lisk
* @task load Loading Objects
* @task info Examining Objects
* @task save Writing Objects
* @task hook Hooks and Callbacks
* @task util Utilities
* @task xaction Managing Transactions
* @task isolate Isolation for Unit Testing
*
* @group storage
*/
abstract
class
LiskDAO
{
const
CONFIG_OPTIMISTIC_LOCKS
=
'enable-locks'
;
const
CONFIG_IDS
=
'id-mechanism'
;
const
CONFIG_TIMESTAMPS
=
'timestamps'
;
const
CONFIG_AUX_PHID
=
'auxiliary-phid'
;
const
CONFIG_SERIALIZATION
=
'col-serialization'
;
const
CONFIG_PARTIAL_OBJECTS
=
'partial-objects'
;
const
SERIALIZATION_NONE
=
'id'
;
const
SERIALIZATION_JSON
=
'json'
;
const
SERIALIZATION_PHP
=
'php'
;
const
IDS_AUTOINCREMENT
=
'ids-auto'
;
const
IDS_PHID
=
'ids-phid'
;
const
IDS_MANUAL
=
'ids-manual'
;
private
$__dirtyFields
=
array
();
private
$__missingFields
=
array
();
private
static
$processIsolationLevel
=
0
;
private
static
$transactionIsolationLevel
=
0
;
private
static
$__checkedClasses
=
array
();
private
$__ephemeral
=
false
;
private
static
$connections
=
array
();
private
$inSet
=
null
;
/**
* Build an empty object.
*
* @return obj Empty object.
*/
public
function
__construct
()
{
$id_key
=
$this
->
getIDKey
();
if
(
$id_key
)
{
$this
->
$id_key
=
null
;
}
if
(
$this
->
getConfigOption
(
self
::
CONFIG_PARTIAL_OBJECTS
))
{
$this
->
resetDirtyFields
();
$this_class
=
get_class
(
$this
);
if
(
empty
(
self
::
$__checkedClasses
[
$this_class
]))
{
self
::
$__checkedClasses
=
true
;
if
(
PhabricatorEnv
::
getEnvConfig
(
'lisk.check_property_methods'
))
{
$class
=
new
ReflectionClass
(
get_class
(
$this
));
$methods
=
$class
->
getMethods
();
$properties
=
$this
->
getProperties
();
foreach
(
$methods
as
$method
)
{
$name
=
strtolower
(
$method
->
getName
());
if
(!(
strncmp
(
$name
,
'get'
,
3
)
&&
strncmp
(
$name
,
'set'
,
3
)))
{
$name
=
substr
(
$name
,
3
);
$declaring_class_name
=
$method
->
getDeclaringClass
()->
getName
();
if
(
isset
(
$properties
[
$name
])
&&
$declaring_class_name
!==
'LiskDAO'
)
{
throw
new
Exception
(
"Cannot implement method {$method->getName()} in "
.
"{$declaring_class_name}."
);
}
}
}
}
}
}
}
/* -( Managing Connections )----------------------------------------------- */
/**
* Establish a live connection to a database service. This method should
* return a new connection. Lisk handles connection caching and management;
* do not perform caching deeper in the stack.
*
* @param string Mode, either 'r' (reading) or 'w' (reading and writing).
* @return AphrontDatabaseConnection New database connection.
* @task conn
*/
abstract
protected
function
establishLiveConnection
(
$mode
);
/**
* Return a namespace for this object's connections in the connection cache.
* Generally, the database name is appropriate. Two connections are considered
* equivalent if they have the same connection namespace and mode.
*
* @return string Connection namespace for cache
* @task conn
*/
abstract
protected
function
getConnectionNamespace
();
/**
* Get an existing, cached connection for this object.
*
* @param mode Connection mode.
* @return AprontDatabaseConnection|null Connection, if it exists in cache.
* @task conn
*/
protected
function
getEstablishedConnection
(
$mode
)
{
$key
=
$this
->
getConnectionNamespace
().
':'
.
$mode
;
if
(
isset
(
self
::
$connections
[
$key
]))
{
return
self
::
$connections
[
$key
];
}
return
null
;
}
/**
* Store a connection in the connection cache.
*
* @param mode Connection mode.
* @param AphrontDatabaseConnection Connection to cache.
* @return this
* @task conn
*/
protected
function
setEstablishedConnection
(
$mode
,
AphrontDatabaseConnection
$connection
)
{
$key
=
$this
->
getConnectionNamespace
().
':'
.
$mode
;
self
::
$connections
[
$key
]
=
$connection
;
return
$this
;
}
/* -( Configuring Lisk )--------------------------------------------------- */
/**
* Change Lisk behaviors, like optimistic locks and timestamps. If you want
* to change these behaviors, you should override this method in your child
* class and change the options you're interested in. For example:
*
* public function getConfiguration() {
* return array(
* Lisk_DataAccessObject::CONFIG_EXAMPLE => true,
* ) + parent::getConfiguration();
* }
*
* The available options are:
*
* CONFIG_OPTIMISTIC_LOCKS
* Lisk automatically performs optimistic locking on objects, which protects
* you from read-modify-write concurrency problems. Lock failures are
* detected at write time and arise when two users read an object, then both
* save it. In theory, you should detect these failures and accommodate them
* in some sensible way (for instance, by showing the user differences
* between the original record and the copy they are trying to update, and
* prompting them to merge them). In practice, most Lisk tools are quick
* and dirty and don't get to that level of sophistication, but optimistic
* locks can still protect you from yourself sometimes. If you don't want
* to use optimistic locks, you can disable them. The performance cost of
* doing this locking is very very small (optimistic locks were chosen
* because they're simple and cheap, and highly optimized for the case where
* collisions are rare). By default, this option is OFF.
*
* CONFIG_IDS
* Lisk objects need to have a unique identifying ID. The three mechanisms
* available for generating this ID are IDS_AUTOINCREMENT (default, assumes
* the ID column is an autoincrement primary key), IDS_PHID (to generate a
* unique PHID for each object) or IDS_MANUAL (you are taking full
* responsibility for ID management).
*
* CONFIG_TIMESTAMPS
* Lisk can automatically handle keeping track of a `dateCreated' and
* `dateModified' column, which it will update when it creates or modifies
* an object. If you don't want to do this, you may disable this option.
* By default, this option is ON.
*
* CONFIG_AUX_PHID
* This option can be enabled by being set to some truthy value. The meaning
* of this value is defined by your PHID generation mechanism. If this option
* is enabled, a `phid' property will be populated with a unique PHID when an
* object is created (or if it is saved and does not currently have one). You
* need to override generatePHID() and hook it into your PHID generation
* mechanism for this to work. By default, this option is OFF.
*
* CONFIG_SERIALIZATION
* You can optionally provide a column serialization map that will be applied
* to values when they are written to the database. For example:
*
* self::CONFIG_SERIALIZATION => array(
* 'complex' => self::SERIALIZATION_JSON,
* )
*
* This will cause Lisk to JSON-serialize the 'complex' field before it is
* written, and unserialize it when it is read.
*
* CONFIG_PARTIAL_OBJECTS
* Sometimes, it is useful to load only some fields of an object (such as
* when you are loading all objects of a class, but only care about a few
* fields). Turning on this option (by setting it to a truthy value) allows
* users of the class to create/use partial objects, but it comes with some
* side effects: your class cannot override the setters and getters provided
* by Lisk (use readField and writeField instead), and you should not
* directly access or assign protected members of your class (use the getters
* and setters).
*
*
* @return dictionary Map of configuration options to values.
*
* @task config
*/
protected
function
getConfiguration
()
{
return
array
(
self
::
CONFIG_OPTIMISTIC_LOCKS
=>
false
,
self
::
CONFIG_IDS
=>
self
::
IDS_AUTOINCREMENT
,
self
::
CONFIG_TIMESTAMPS
=>
true
,
self
::
CONFIG_PARTIAL_OBJECTS
=>
false
,
);
}
/**
* Determine the setting of a configuration option for this class of objects.
*
* @param const Option name, one of the CONFIG_* constants.
* @return mixed Option value, if configured (null if unavailable).
*
* @task config
*/
public
function
getConfigOption
(
$option_name
)
{
static
$options
=
null
;
if
(!
isset
(
$options
))
{
$options
=
$this
->
getConfiguration
();
}
return
idx
(
$options
,
$option_name
);
}
/* -( Loading Objects )---------------------------------------------------- */
/**
* Load an object by ID. You need to invoke this as an instance method, not
* a class method, because PHP doesn't have late static binding (until
* PHP 5.3.0). For example:
*
* $dog = id(new Dog())->load($dog_id);
*
* @param int Numeric ID identifying the object to load.
* @return obj|null Identified object, or null if it does not exist.
*
* @task load
*/
public
function
load
(
$id
)
{
if
(!(
$id
=
(
int
)
$id
))
{
throw
new
Exception
(
"Bogus ID provided to load()."
);
}
return
$this
->
loadOneWhere
(
'%C = %d'
,
$this
->
getIDKeyForUse
(),
$id
);
}
/**
* Loads all of the objects, unconditionally.
*
* @return dict Dictionary of all persisted objects of this type, keyed
* on object ID.
*
* @task load
*/
public
function
loadAll
()
{
return
$this
->
loadAllWhere
(
'1 = 1'
);
}
/**
* Loads all objects, but only fetches the specified columns.
*
* @param array Array of canonical column names as strings
* @return dict Dictionary of all objects, keyed by ID.
*
* @task load
*/
public
function
loadColumns
(
array
$columns
)
{
return
$this
->
loadColumnsWhere
(
$columns
,
'1 = 1'
);
}
/**
* Load all objects which match a WHERE clause. You provide everything after
* the 'WHERE'; Lisk handles everything up to it. For example:
*
* $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7);
*
* The pattern and arguments are as per queryfx().
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return dict Dictionary of matching objects, keyed on ID.
*
* @task load
*/
public
function
loadAllWhere
(
$pattern
/*, $arg, $arg, $arg ... */
)
{
$args
=
func_get_args
();
array_unshift
(
$args
,
null
);
$data
=
call_user_func_array
(
array
(
$this
,
'loadRawDataWhere'
),
$args
);
return
$this
->
loadAllFromArray
(
$data
);
}
/**
* Loads selected columns from objects that match a WHERE clause. You must
* provide everything after the WHERE. See loadAllWhere().
*
* @param array List of column names.
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return dict Dictionary of matching objecks, keyed by ID.
*
* @task load
*/
public
function
loadColumnsWhere
(
array
$columns
,
$pattern
/*, $args... */
)
{
if
(!
$this
->
getConfigOption
(
self
::
CONFIG_PARTIAL_OBJECTS
))
{
throw
new
BadMethodCallException
(
"This class does not support partial objects."
);
}
$args
=
func_get_args
();
$data
=
call_user_func_array
(
array
(
$this
,
'loadRawDataWhere'
),
$args
);
return
$this
->
loadAllFromArray
(
$data
);
}
/**
* Load a single object identified by a 'WHERE' clause. You provide
* everything after the 'WHERE', and Lisk builds the first half of the
* query. See loadAllWhere(). This method is similar, but returns a single
* result instead of a list.
*
* @param string queryfx()-style SQL WHERE clause.
* @param ... Zero or more conversions.
* @return obj|null Matching object, or null if no object matches.
*
* @task load
*/
public
function
loadOneWhere
(
$pattern
/*, $arg, $arg, $arg ... */
)
{
$args
=
func_get_args
();
array_unshift
(
$args
,
null
);
$data
=
call_user_func_array
(
array
(
$this
,
'loadRawDataWhere'
),
$args
);
if
(
count
(
$data
)
>
1
)
{
throw
new
AphrontQueryCountException
(
"More than 1 result from loadOneWhere()!"
);
}
$data
=
reset
(
$data
);
if
(!
$data
)
{
return
null
;
}
return
$this
->
loadFromArray
(
$data
);
}
protected
function
loadRawDataWhere
(
$columns
,
$pattern
/*, $args... */
)
{
$connection
=
$this
->
establishConnection
(
'r'
);
$lock_clause
=
''
;
if
(
$connection
->
isReadLocking
())
{
$lock_clause
=
'FOR UPDATE'
;
}
else
if
(
$connection
->
isWriteLocking
())
{
$lock_clause
=
'LOCK IN SHARE MODE'
;
}
$args
=
func_get_args
();
$args
=
array_slice
(
$args
,
2
);
if
(!
$columns
)
{
$column
=
'*'
;
}
else
{
$column
=
'%LC'
;
$columns
[]
=
$this
->
getIDKey
();
$properties
=
$this
->
getProperties
();
$this
->
__missingFields
=
array_diff_key
(
array_flip
(
$properties
),
array_flip
(
$columns
));
}
$pattern
=
'SELECT '
.
$column
.
' FROM %T WHERE '
.
$pattern
.
' %Q'
;
array_unshift
(
$args
,
$this
->
getTableName
());
if
(
$columns
)
{
array_unshift
(
$args
,
$columns
);
}
array_push
(
$args
,
$lock_clause
);
array_unshift
(
$args
,
$pattern
);
return
call_user_func_array
(
array
(
$connection
,
'queryData'
),
$args
);
}
/**
* Reload an object from the database, discarding any changes to persistent
* properties. If the object uses optimistic locks and you are in a locking
* mode while transactional, this will effectively synchronize the locks.
* This is pretty heady. It is unlikely you need to use this method.
*
* @return this
*
* @task load
*/
public
function
reload
()
{
if
(!
$this
->
getID
())
{
throw
new
Exception
(
"Unable to reload object that hasn't been loaded!"
);
}
$use_locks
=
$this
->
getConfigOption
(
self
::
CONFIG_OPTIMISTIC_LOCKS
);
if
(!
$use_locks
)
{
$result
=
$this
->
loadOneWhere
(
'%C = %d'
,
$this
->
getIDKeyForUse
(),
$this
->
getID
());
}
else
{
$result
=
$this
->
loadOneWhere
(
'%C = %d AND %C = %d'
,
$this
->
getIDKeyForUse
(),
$this
->
getID
(),
'version'
,
$this
->
getVersion
());
}
if
(!
$result
)
{
throw
new
AphrontQueryObjectMissingException
(
$use_locks
);
}
return
$this
;
}
/**
* Initialize this object's properties from a dictionary. Generally, you
* load single objects with loadOneWhere(), but sometimes it may be more
* convenient to pull data from elsewhere directly (e.g., a complicated
* join via queryData()) and then load from an array representation.
*
* @param dict Dictionary of properties, which should be equivalent to
* selecting a row from the table or calling getProperties().
* @return this
*
* @task load
*/
public
function
loadFromArray
(
array
$row
)
{
// TODO: We should load only valid properties.
$map
=
array
();
foreach
(
$row
as
$k
=>
$v
)
{
$map
[
$k
]
=
$v
;
}
$this
->
willReadData
(
$map
);
foreach
(
$map
as
$prop
=>
$value
)
{
$this
->
$prop
=
$value
;
}
$this
->
didReadData
();
return
$this
;
}
/**
* Initialize a list of objects from a list of dictionaries. Usually you
* load lists of objects with loadAllWhere(), but sometimes that isn't
* flexible enough. One case is if you need to do joins to select the right
* objects:
*
* function loadAllWithOwner($owner) {
* $data = $this->queryData(
* 'SELECT d.*
* FROM owner o
* JOIN owner_has_dog od ON o.id = od.ownerID
* JOIN dog d ON od.dogID = d.id
* WHERE o.id = %d',
* $owner);
* return $this->loadAllFromArray($data);
* }
*
* This is a lot messier than loadAllWhere(), but more flexible.
*
* @param list List of property dictionaries.
* @return dict List of constructed objects, keyed on ID.
*
* @task load
*/
public
function
loadAllFromArray
(
array
$rows
)
{
$result
=
array
();
$id_key
=
$this
->
getIDKey
();
foreach
(
$rows
as
$row
)
{
$obj
=
clone
$this
;
if
(
$id_key
&&
isset
(
$row
[
$id_key
]))
{
$result
[
$row
[
$id_key
]]
=
$obj
->
loadFromArray
(
$row
);
}
else
{
$result
[]
=
$obj
->
loadFromArray
(
$row
);
}
if
(
$this
->
inSet
)
{
$this
->
inSet
->
addToSet
(
$obj
);
}
}
return
$result
;
}
/**
* This method helps to prevent the 1+N queries problem. It happens when you
* execute a query for each row in a result set. Like in this code:
*
* COUNTEREXAMPLE, name=Easy to write but expensive to execute
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* foreach ($diffs as $diff) {
* $changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID = %d',
* $diff->getID());
* // Do something with $changesets.
* }
*
* One can solve this problem by reading all the dependent objects at once and
* assigning them later:
*
* COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain
* $diffs = id(new DifferentialDiff())->loadAllWhere(
* 'revisionID = %d',
* $revision->getID());
* $all_changesets = id(new DifferentialChangeset())->loadAllWhere(
* 'diffID IN (%Ld)',
* mpull($diffs, 'getID'));
* $all_changesets = mgroup($all_changesets, 'getDiffID');
* foreach ($diffs as $diff) {
* $changesets = idx($all_changesets, $diff->getID(), array());
* // Do something with $changesets.
* }
*
* The method @{method:loadRelatives} abstracts this approach which allows
* writing a code which is simple and efficient at the same time:
*
* name=Easy to write and cheap to execute
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* // Do something with $changesets.
* }
*
* This will load dependent objects for all diffs in the first call of
* @{method:loadRelatives} and use this result for all following calls.
*
* The method supports working with set of sets, like in this code:
*
* $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID');
* foreach ($diffs as $diff) {
* $changesets = $diff->loadRelatives(
* new DifferentialChangeset(),
* 'diffID');
* foreach ($changesets as $changeset) {
* $hunks = $changeset->loadRelatives(
* new DifferentialHunk(),
* 'changesetID');
* // Do something with hunks.
* }
* }
*
* This code will execute just three queries - one to load all diffs, one to
* load all their related changesets and one to load all their related hunks.
* You can try to write an equivalent code without using this method as
* a homework.
*
* The method also supports retrieving referenced objects, for example authors
* of all diffs:
*
* foreach ($diffs as $diff) {
* $author = head($diff->loadRelatives(
* new PhabricatorUser(),
* 'phid',
* 'getAuthorPHID'));
* // Do something with author.
* }
*
* It is also possible to specify additional conditions for the `WHERE`
* clause. Similarly to @{method:loadAllWhere}, you can specify everything
* after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is
* allowed to pass only a constant string (`%` doesn't have a special
* meaning). This is intentional to avoid mistakes with using data from one
* row in retrieving other rows. Example of a correct usage:
*
* $status = head($author->loadRelatives(
* new PhabricatorUserStatus(),
* 'userPHID',
* 'getPHID',
* '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'));
*
* @param LiskDAO Type of objects to load.
* @param string Name of the column in target table.
* @param string Method name in this table.
* @param string Additional constraints on returned rows. It supports no
* placeholders and requires putting the WHERE part into
* parentheses. It's not possible to use LIMIT.
* @return list Objects of type $object.
*
* @task load
*/
public
function
loadRelatives
(
LiskDAO
$object
,
$foreign_column
,
$key_method
=
'getID'
,
$where
=
''
)
{
if
(!
$this
->
inSet
)
{
id
(
new
LiskDAOSet
())->
addToSet
(
$this
);
}
$relatives
=
$this
->
inSet
->
loadRelatives
(
$object
,
$foreign_column
,
$key_method
,
$where
);
return
idx
(
$relatives
,
$this
->
$key_method
(),
array
());
}
final
public
function
putInSet
(
LiskDAOSet
$set
)
{
$this
->
inSet
=
$set
;
return
$this
;
}
/* -( Examining Objects )-------------------------------------------------- */
/**
* Retrieve the unique, numerical ID identifying this object. This value
* will be null if the object hasn't been persisted.
*
* @return int Unique numerical ID.
*
* @task info
*/
public
function
getID
()
{
static
$id_key
=
null
;
if
(
$id_key
===
null
)
{
$id_key
=
$this
->
getIDKeyForUse
();
}
return
$this
->
$id_key
;
}
/**
* Retrieve a list of all object properties. This list only includes
* properties that are declared as protected, and it is expected that
* all properties returned by this function should be persisted to the
* database.
* Properties that should not be persisted must be declared as private.
*
* @return dict Dictionary of normalized (lowercase) to canonical (original
* case) property names.
*
* @task info
*/
protected
function
getProperties
()
{
static
$properties
=
null
;
if
(!
isset
(
$properties
))
{
$class
=
new
ReflectionClass
(
get_class
(
$this
));
$properties
=
array
();
foreach
(
$class
->
getProperties
(
ReflectionProperty
::
IS_PROTECTED
)
as
$p
)
{
$properties
[
strtolower
(
$p
->
getName
())]
=
$p
->
getName
();
}
$id_key
=
$this
->
getIDKey
();
if
(
$id_key
)
{
if
(!
isset
(
$properties
[
strtolower
(
$id_key
)]))
{
$properties
[
strtolower
(
$id_key
)]
=
$id_key
;
}
}
if
(
$this
->
getConfigOption
(
self
::
CONFIG_OPTIMISTIC_LOCKS
))
{
$properties
[
'version'
]
=
'version'
;
}
if
(
$this
->
getConfigOption
(
self
::
CONFIG_TIMESTAMPS
))
{
$properties
[
'datecreated'
]
=
'dateCreated'
;
$properties
[
'datemodified'
]
=
'dateModified'
;
}
if
(!
$this
->
isPHIDPrimaryID
()
&&
$this
->
getConfigOption
(
self
::
CONFIG_AUX_PHID
))
{
$properties
[
'phid'
]
=
'phid'
;
}
}
return
$properties
;
}
/**
* Check if a property exists on this object.
*
* @return string|null Canonical property name, or null if the property
* does not exist.
*
* @task info
*/
protected
function
checkProperty
(
$property
)
{
static
$properties
=
null
;
if
(
$properties
===
null
)
{
$properties
=
$this
->
getProperties
();
}
$property
=
strtolower
(
$property
);
if
(
empty
(
$properties
[
$property
]))
{
return
null
;
}
return
$properties
[
$property
];
}
/**
* Get or build the database connection for this object.
*
* @return LiskDatabaseConnection Lisk connection object.
*
* @task info
*/
public
function
establishConnection
(
$mode
)
{
if
(
$mode
!=
'r'
&&
$mode
!=
'w'
)
{
throw
new
Exception
(
"Unknown mode '{$mode}', should be 'r' or 'w'."
);
}
if
(
self
::
shouldIsolateAllLiskEffectsToCurrentProcess
())
{
$mode
=
'isolate-'
.
$mode
;
$connection
=
$this
->
getEstablishedConnection
(
$mode
);
if
(!
$connection
)
{
$connection
=
$this
->
establishIsolatedConnection
(
$mode
);
$this
->
setEstablishedConnection
(
$mode
,
$connection
);
}
return
$connection
;
}
if
(
self
::
shouldIsolateAllLiskEffectsToTransactions
())
{
// If we're doing fixture transaction isolation, force the mode to 'w'
// so we always get the same connection for reads and writes, and thus
// can see the writes inside the transaction.
$mode
=
'w'
;
}
// TODO: There is currently no protection on 'r' queries against writing.
$connection
=
null
;
if
(
$mode
==
'r'
)
{
// If we're requesting a read connection but already have a write
// connection, reuse the write connection so that reads can take place
// inside transactions.
$connection
=
$this
->
getEstablishedConnection
(
'w'
);
}
if
(!
$connection
)
{
$connection
=
$this
->
getEstablishedConnection
(
$mode
);
}
if
(!
$connection
)
{
$connection
=
$this
->
establishLiveConnection
(
$mode
);
if
(
self
::
shouldIsolateAllLiskEffectsToTransactions
())
{
$connection
->
openTransaction
();
}
$this
->
setEstablishedConnection
(
$mode
,
$connection
);
}
return
$connection
;
}
/**
* Convert this object into a property dictionary. This dictionary can be
* restored into an object by using loadFromArray() (unless you're using
* legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should
* just go ahead and die in a fire).
*
* @return dict Dictionary of object properties.
*
* @task info
*/
protected
function
getPropertyValues
()
{
$map
=
array
();
foreach
(
$this
->
getProperties
()
as
$p
)
{
// We may receive a warning here for properties we've implicitly added
// through configuration; squelch it.
$map
[
$p
]
=
@
$this
->
$p
;
}
return
$map
;
}
/* -( Writing Objects )---------------------------------------------------- */
/**
* Make an object read-only.
*
* Making an object ephemeral indicates that you will be changing state in
* such a way that you would never ever want it to be written back to the
* storage.
*/
public
function
makeEphemeral
()
{
$this
->
__ephemeral
=
true
;
return
$this
;
}
private
function
isEphemeralCheck
()
{
if
(
$this
->
__ephemeral
)
{
throw
new
LiskEphemeralObjectException
();
}
}
/**
* Persist this object to the database. In most cases, this is the only
* method you need to call to do writes. If the object has not yet been
* inserted this will do an insert; if it has, it will do an update.
*
* @return this
*
* @task save
*/
public
function
save
()
{
if
(
$this
->
shouldInsertWhenSaved
())
{
return
$this
->
insert
();
}
else
{
return
$this
->
update
();
}
}
/**
* Save this object, forcing the query to use REPLACE regardless of object
* state.
*
* @return this
*
* @task save
*/
public
function
replace
()
{
$this
->
isEphemeralCheck
();
return
$this
->
insertRecordIntoDatabase
(
'REPLACE'
);
}
/**
* Save this object, forcing the query to use INSERT regardless of object
* state.
*
* @return this
*
* @task save
*/
public
function
insert
()
{
$this
->
isEphemeralCheck
();
return
$this
->
insertRecordIntoDatabase
(
'INSERT'
);
}
/**
* Save this object, forcing the query to use UPDATE regardless of object
* state.
*
* @return this
*
* @task save
*/
public
function
update
()
{
$this
->
isEphemeralCheck
();
$use_locks
=
$this
->
getConfigOption
(
self
::
CONFIG_OPTIMISTIC_LOCKS
);
$this
->
willSaveObject
();
$data
=
$this
->
getPropertyValues
();
if
(
$this
->
getConfigOption
(
self
::
CONFIG_PARTIAL_OBJECTS
))
{
$data
=
array_intersect_key
(
$data
,
$this
->
__dirtyFields
);
}
$this
->
willWriteData
(
$data
);
$map
=
array
();
foreach
(
$data
as
$k
=>
$v
)
{
if
(
$use_locks
&&
$k
==
'version'
)
{
continue
;
}
$map
[
$k
]
=
$v
;
}
$conn
=
$this
->
establishConnection
(
'w'
);
foreach
(
$map
as
$key
=>
$value
)
{
$map
[
$key
]
=
qsprintf
(
$conn
,
'%C = %ns'
,
$key
,
$value
);
}
$map
=
implode
(
', '
,
$map
);
if
(
$use_locks
)
{
$conn
->
query
(
'UPDATE %T SET %Q, version = version + 1 WHERE %C = %d AND %C = %d'
,
$this
->
getTableName
(),
$map
,
$this
->
getIDKeyForUse
(),
$this
->
getID
(),
'version'
,
$this
->
getVersion
());
if
(
$conn
->
getAffectedRows
()
!==
1
)
{
throw
new
AphrontQueryObjectMissingException
(
$use_locks
);
}
$this
->
setVersion
(
$this
->
getVersion
()
+
1
);
}
else
{
$id
=
$this
->
getID
();
$conn
->
query
(
'UPDATE %T SET %Q WHERE %C = '
.(
is_int
(
$id
)
?
'%d'
:
'%s'
),
$this
->
getTableName
(),
$map
,
$this
->
getIDKeyForUse
(),
$id
);
// We can't detect a missing object because updating an object without
// changing any values doesn't affect rows. We could jiggle timestamps
// to catch this for objects which track them if we wanted.
}
$this
->
didWriteData
();
if
(
$this
->
getConfigOption
(
self
::
CONFIG_PARTIAL_OBJECTS
))
{
$this
->
resetDirtyFields
();
}
return
$this
;
}
/**
* Delete this object, permanently.
*
* @return this
*
* @task save
*/
public
function
delete
()
{
$this
->
isEphemeralCheck
();
$this
->
willDelete
();
$conn
=
$this
->
establishConnection
(
'w'
);
$conn
->
query
(
'DELETE FROM %T WHERE %C = %d'
,
$this
->
getTableName
(),
$this
->
getIDKeyForUse
(),
$this
->
getID
());
$this
->
didDelete
();
return
$this
;
}
/**
* Internal implementation of INSERT and REPLACE.
*
* @param const Either "INSERT" or "REPLACE", to force the desired mode.
*
* @task save
*/
protected
function
insertRecordIntoDatabase
(
$mode
)
{
$this
->
willSaveObject
();
$data
=
$this
->
getPropertyValues
();
$id_mechanism
=
$this
->
getConfigOption
(
self
::
CONFIG_IDS
);
switch
(
$id_mechanism
)
{
case
self
::
IDS_AUTOINCREMENT
:
// If we are using autoincrement IDs, let MySQL assign the value for the
// ID column, if it is empty. If the caller has explicitly provided a
// value, use it.
$id_key
=
$this
->
getIDKeyForUse
();
if
(
empty
(
$data
[
$id_key
]))
{
unset
(
$data
[
$id_key
]);
}
break
;
case
self
::
IDS_PHID
:
if
(
empty
(
$data
[
$this
->
getIDKeyForUse
()]))
{
$phid
=
$this
->
generatePHID
();
$this
->
setID
(
$phid
);
$data
[
$this
->
getIDKeyForUse
()]
=
$phid
;
}
break
;
case
self
::
IDS_MANUAL
:
break
;
default
:
throw
new
Exception
(
'Unknown CONFIG_IDs mechanism!'
);
}
if
(
$this
->
getConfigOption
(
self
::
CONFIG_OPTIMISTIC_LOCKS
))
{
$data
[
'version'
]
=
0
;
}
$this
->
willWriteData
(
$data
);
$conn
=
$this
->
establishConnection
(
'w'
);
$columns
=
array_keys
(
$data
);
foreach
(
$data
as
$key
=>
$value
)
{
$data
[
$key
]
=
qsprintf
(
$conn
,
'%ns'
,
$value
);
}
$data
=
implode
(
', '
,
$data
);
$conn
->
query
(
'%Q INTO %T (%LC) VALUES (%Q)'
,
$mode
,
$this
->
getTableName
(),
$columns
,
$data
);
// Update the object with the initial Version value
if
(
$this
->
getConfigOption
(
self
::
CONFIG_OPTIMISTIC_LOCKS
))
{
$this
->
setVersion
(
0
);
}
// Only use the insert id if this table is using auto-increment ids
if
(
$id_mechanism
===
self
::
IDS_AUTOINCREMENT
)
{
$this
->
setID
(
$conn
->
getInsertID
());
}
$this
->
didWriteData
();
if
(
$this
->
getConfigOption
(
self
::
CONFIG_PARTIAL_OBJECTS
))
{
$this
->
resetDirtyFields
();
}
return
$this
;
}
/**
* Method used to determine whether to insert or update when saving.
*
* @return bool true if the record should be inserted
*/
protected
function
shouldInsertWhenSaved
()
{
$key_type
=
$this
->
getConfigOption
(
self
::
CONFIG_IDS
);
$use_locks
=
$this
->
getConfigOption
(
self
::
CONFIG_OPTIMISTIC_LOCKS
);
if
(
$key_type
==
self
::
IDS_MANUAL
)
{
if
(
$use_locks
)
{
// If we are manually keyed and the object has a version (which means
// that it has been saved to the DB before), do an update, otherwise
// perform an insert.
if
(
$this
->
getID
()
&&
$this
->
getVersion
()
!==
null
)
{
return
false
;
}
else
{
return
true
;
}
}
else
{
throw
new
Exception
(
'You are not using optimistic locks, but are using manual IDs. You '
.
'must override the shouldInsertWhenSaved() method to properly '
.
'detect when to insert a new record.'
);
}
}
else
{
return
!
$this
->
getID
();
}
}
/* -( Hooks and Callbacks )------------------------------------------------ */
/**
* Retrieve the database table name. By default, this is the class name.
*
* @return string Table name for object storage.
*
* @task hook
*/
public
function
getTableName
()
{
return
get_class
(
$this
);
}
/**
* Helper: Whether this class is configured to use PHIDs as the primary ID.
* @task internal
*/
private
function
isPHIDPrimaryID
()
{
return
(
$this
->
getConfigOption
(
self
::
CONFIG_IDS
)
===
self
::
IDS_PHID
);
}
/**
* Retrieve the primary key column, "id" by default. If you can not
* reasonably name your ID column "id", override this method.
*
* @return string Name of the ID column.
*
* @task hook
*/
public
function
getIDKey
()
{
return
$this
->
isPHIDPrimaryID
()
?
'phid'
:
'id'
;
}
protected
function
getIDKeyForUse
()
{
$id_key
=
$this
->
getIDKey
();
if
(!
$id_key
)
{
throw
new
Exception
(
"This DAO does not have a single-part primary key. The method you "
.
"called requires a single-part primary key."
);
}
return
$id_key
;
}
/**
* Generate a new PHID, used by CONFIG_AUX_PHID and IDS_PHID.
*
* @return phid Unique, newly allocated PHID.
*
* @task hook
*/
protected
function
generatePHID
()
{
throw
new
Exception
(
"To use CONFIG_AUX_PHID or IDS_PHID, you need to overload "
.
"generatePHID() to perform PHID generation."
);
}
/**
* Hook to apply serialization or validation to data before it is written to
* the database. See also willReadData().
*
* @task hook
*/
protected
function
willWriteData
(
array
&
$data
)
{
$this
->
applyLiskDataSerialization
(
$data
,
false
);
}
/**
* Hook to perform actions after data has been written to the database.
*
* @task hook
*/
protected
function
didWriteData
()
{}
/**
* Hook to make internal object state changes prior to INSERT, REPLACE or
* UPDATE.
*
* @task hook
*/
protected
function
willSaveObject
()
{
$use_timestamps
=
$this
->
getConfigOption
(
self
::
CONFIG_TIMESTAMPS
);
if
(
$use_timestamps
)
{
if
(!
$this
->
getDateCreated
())
{
$this
->
setDateCreated
(
time
());
}
$this
->
setDateModified
(
time
());
}
if
((
$this
->
isPHIDPrimaryID
()
&&
!
$this
->
getID
()))
{
// If PHIDs are the primary ID, the subclass could have overridden the
// name of the ID column.
$this
->
setID
(
$this
->
generatePHID
());
}
else
if
(
$this
->
getConfigOption
(
self
::
CONFIG_AUX_PHID
)
&&
!
$this
->
getPHID
())
{
// The subclass could still want PHIDs.
$this
->
setPHID
(
$this
->
generatePHID
());
}
}
/**
* Hook to apply serialization or validation to data as it is read from the
* database. See also willWriteData().
*
* @task hook
*/
protected
function
willReadData
(
array
&
$data
)
{
$this
->
applyLiskDataSerialization
(
$data
,
$deserialize
=
true
);
}
/**
* Hook to perform an action on data after it is read from the database.
*
* @task hook
*/
protected
function
didReadData
()
{}
/**
* Hook to perform an action before the deletion of an object.
*
* @task hook
*/
protected
function
willDelete
()
{}
/**
* Hook to perform an action after the deletion of an object.
*
* @task hook
*/
protected
function
didDelete
()
{}
/**
* Reads the value from a field. Override this method for custom behavior
* of getField() instead of overriding getField directly.
*
* @param string Canonical field name
* @return mixed Value of the field
*
* @task hook
*/
protected
function
readField
(
$field
)
{
if
(
isset
(
$this
->
$field
))
{
return
$this
->
$field
;
}
return
null
;
}
/**
* Writes a value to a field. Override this method for custom behavior of
* setField($value) instead of overriding setField directly.
*
* @param string Canonical field name
* @param mixed Value to write
*
* @task hook
*/
protected
function
writeField
(
$field
,
$value
)
{
$this
->
$field
=
$value
;
}
/* -( Manging Transactions )----------------------------------------------- */
/**
* Increase transaction stack depth.
*
* @return this
*/
public
function
openTransaction
()
{
$this
->
establishConnection
(
'w'
)->
openTransaction
();
return
$this
;
}
/**
* Decrease transaction stack depth, saving work.
*
* @return this
*/
public
function
saveTransaction
()
{
$this
->
establishConnection
(
'w'
)->
saveTransaction
();
return
$this
;
}
/**
* Decrease transaction stack depth, discarding work.
*
* @return this
*/
public
function
killTransaction
()
{
$this
->
establishConnection
(
'w'
)->
killTransaction
();
return
$this
;
}
/**
* Begins read-locking selected rows with SELECT ... FOR UPDATE, so that
* other connections can not read them (this is an enormous oversimplification
* of FOR UPDATE semantics; consult the MySQL documentation for details). To
* end read locking, call @{method:endReadLocking}. For example:
*
* $beach->openTransaction();
* $beach->beginReadLocking();
*
* $beach->reload();
* $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1);
* $beach->save();
*
* $beach->endReadLocking();
* $beach->saveTransaction();
*
* @return this
* @task xaction
*/
public
function
beginReadLocking
()
{
$this
->
establishConnection
(
'w'
)->
beginReadLocking
();
return
$this
;
}
/**
* Ends read-locking that began at an earlier @{method:beginReadLocking} call.
*
* @return this
* @task xaction
*/
public
function
endReadLocking
()
{
$this
->
establishConnection
(
'w'
)->
endReadLocking
();
return
$this
;
}
/**
* Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so
* that other connections can not update or delete them (this is an
* oversimplification of LOCK IN SHARE MODE semantics; consult the
* MySQL documentation for details). To end write locking, call
* @{method:endWriteLocking}.
*
* @return this
* @task xaction
*/
public
function
beginWriteLocking
()
{
$this
->
establishConnection
(
'w'
)->
beginWriteLocking
();
return
$this
;
}
/**
* Ends write-locking that began at an earlier @{method:beginWriteLocking}
* call.
*
* @return this
* @task xaction
*/
public
function
endWriteLocking
()
{
$this
->
establishConnection
(
'w'
)->
endWriteLocking
();
return
$this
;
}
/* -( Isolation )---------------------------------------------------------- */
/**
* @task isolate
*/
public
static
function
beginIsolateAllLiskEffectsToCurrentProcess
()
{
self
::
$processIsolationLevel
++;
}
/**
* @task isolate
*/
public
static
function
endIsolateAllLiskEffectsToCurrentProcess
()
{
self
::
$processIsolationLevel
--;
if
(
self
::
$processIsolationLevel
<
0
)
{
throw
new
Exception
(
"Lisk process isolation level was reduced below 0."
);
}
}
/**
* @task isolate
*/
public
static
function
shouldIsolateAllLiskEffectsToCurrentProcess
()
{
return
(
bool
)
self
::
$processIsolationLevel
;
}
/**
* @task isolate
*/
private
function
establishIsolatedConnection
(
$mode
)
{
$config
=
array
();
return
new
AphrontIsolatedDatabaseConnection
(
$config
);
}
/**
* @task isolate
*/
public
static
function
beginIsolateAllLiskEffectsToTransactions
()
{
if
(
self
::
$transactionIsolationLevel
===
0
)
{
self
::
closeAllConnections
();
}
self
::
$transactionIsolationLevel
++;
}
/**
* @task isolate
*/
public
static
function
endIsolateAllLiskEffectsToTransactions
()
{
self
::
$transactionIsolationLevel
--;
if
(
self
::
$transactionIsolationLevel
<
0
)
{
throw
new
Exception
(
"Lisk transaction isolation level was reduced below 0."
);
}
else
if
(
self
::
$transactionIsolationLevel
==
0
)
{
foreach
(
self
::
$connections
as
$key
=>
$conn
)
{
if
(
$conn
)
{
$conn
->
killTransaction
();
}
}
self
::
closeAllConnections
();
}
}
/**
* @task isolate
*/
public
static
function
shouldIsolateAllLiskEffectsToTransactions
()
{
return
(
bool
)
self
::
$transactionIsolationLevel
;
}
public
static
function
closeAllConnections
()
{
self
::
$connections
=
array
();
}
/* -( Utilities )---------------------------------------------------------- */
/**
* Applies configured serialization to a dictionary of values.
*
* @task util
*/
protected
function
applyLiskDataSerialization
(
array
&
$data
,
$deserialize
)
{
$serialization
=
$this
->
getConfigOption
(
self
::
CONFIG_SERIALIZATION
);
if
(
$serialization
)
{
foreach
(
array_intersect_key
(
$serialization
,
$data
)
as
$col
=>
$format
)
{
switch
(
$format
)
{
case
self
::
SERIALIZATION_NONE
:
break
;
case
self
::
SERIALIZATION_PHP
:
if
(
$deserialize
)
{
$data
[
$col
]
=
unserialize
(
$data
[
$col
]);
}
else
{
$data
[
$col
]
=
serialize
(
$data
[
$col
]);
}
break
;
case
self
::
SERIALIZATION_JSON
:
if
(
$deserialize
)
{
$data
[
$col
]
=
json_decode
(
$data
[
$col
],
true
);
}
else
{
$data
[
$col
]
=
json_encode
(
$data
[
$col
]);
}
break
;
default
:
throw
new
Exception
(
"Unknown serialization format '{$format}'."
);
}
}
}
}
/**
* Resets the dirty fields (fields which need to be written on next save/
* update/insert/replace). If this DAO has timestamps, the modified time
* is always a dirty field.
*
* @task util
*/
private
function
resetDirtyFields
()
{
$this
->
__dirtyFields
=
array
();
if
(
$this
->
getConfigOption
(
self
::
CONFIG_TIMESTAMPS
))
{
$this
->
__dirtyFields
[
'dateModified'
]
=
true
;
}
}
/**
* Black magic. Builds implied get*() and set*() for all properties.
*
* @param string Method name.
* @param list Argument vector.
* @return mixed get*() methods return the property value. set*() methods
* return $this.
* @task util
*/
public
function
__call
(
$method
,
$args
)
{
// NOTE: This method is very performance-sensitive (many thousands of calls
// per page on some pages), and thus has some silliness in the name of
// optimizations.
static
$dispatch_map
=
array
();
static
$partial
=
null
;
if
(
$partial
===
null
)
{
$partial
=
$this
->
getConfigOption
(
self
::
CONFIG_PARTIAL_OBJECTS
);
}
if
(
$method
[
0
]
===
'g'
)
{
if
(
isset
(
$dispatch_map
[
$method
]))
{
$property
=
$dispatch_map
[
$method
];
}
else
{
if
(
substr
(
$method
,
0
,
3
)
!==
'get'
)
{
throw
new
Exception
(
"Unable to resolve method '{$method}'!"
);
}
$property
=
substr
(
$method
,
3
);
if
(!(
$property
=
$this
->
checkProperty
(
$property
)))
{
throw
new
Exception
(
"Bad getter call: {$method}"
);
}
$dispatch_map
[
$method
]
=
$property
;
}
if
(
$partial
&&
isset
(
$this
->
__missingFields
[
$property
]))
{
throw
new
Exception
(
"Cannot get field that wasn't loaded: {$property}"
);
}
return
$this
->
readField
(
$property
);
}
if
(
$method
[
0
]
===
's'
)
{
if
(
isset
(
$dispatch_map
[
$method
]))
{
$property
=
$dispatch_map
[
$method
];
}
else
{
if
(
substr
(
$method
,
0
,
3
)
!==
'set'
)
{
throw
new
Exception
(
"Unable to resolve method '{$method}'!"
);
}
$property
=
substr
(
$method
,
3
);
$property
=
$this
->
checkProperty
(
$property
);
if
(!
$property
)
{
throw
new
Exception
(
"Bad setter call: {$method}"
);
}
if
(
$property
==
'ID'
)
{
$property
=
$this
->
getIDKeyForUse
();
}
$dispatch_map
[
$method
]
=
$property
;
}
if
(
$partial
)
{
// Accept writes to fields that weren't initially loaded
unset
(
$this
->
__missingFields
[
$property
]);
$this
->
__dirtyFields
[
$property
]
=
true
;
}
$this
->
writeField
(
$property
,
$args
[
0
]);
return
$this
;
}
throw
new
Exception
(
"Unable to resolve method '{$method}'."
);
}
}
Event Timeline
Log In to Comment