View Source

<ac:macro ac:name="unmigrated-inline-wiki-markup"><ac:plain-text-body><![CDATA[{zone-template-instance:ZFPROP:Proposal Zone Template}
{composition-setup}

{zone-data:component-name}
Zend_Db_Mapper
{zone-data}

{zone-data:proposer-list}
[Benjamin Eberlei|mailto:kontakt@beberlei.de]
{zone-data}

{zone-data:liaison}
TBD
{zone-data}

{zone-data:revision}
1.0 - 25 January 2009: Initial Draft.
2.0 - 01 February 2009: Draft Ready for Recommendation
{zone-data}

{zone-data:overview}
h1. Discontinuing Zend Entity

*As a note for everyone finding this proposal: Zend Entity will be discontinued in favour of Doctrine 1 and 2 integration into ZF, see http://www.mail-archive.com/fw-general@lists.zend.com/msg25412.html*

This component willl be an implementation of *DataMapper pattern* that tries to seperate Objects from their persistent separation to let you focus on the application more.

It will offer a huge range of features for handling all types of objects, versioning, relations, cascadading operations. Extension points are provided to make the component as flexible as possible. The data mapping will be based on a mapping syntax that has to be written out explicitly (or be autogenerated). Data Mapping is a crucial point for developing applications that are developed *Domain Driven*. This component will contrast the Table-Row-Data-Gateway pattern which is more useful for Transaction Script or Table Module patterns of the model. Its *core feature* is the *focus on the Entity as datastructure* in contrast to SQL as datastructure. DataMapper makes heavy use of *LazyLoading for relations* that the actual related objects with Proxies that act as if they were the real objects and *only upon request* load the required data from the database. This lazyloading can be done for single related entities, collections or for fields of an entity that contain huge data (BLOBs and Text fields). The lazyloading replacements make sure that every relation of an object can be traversed/accessed inside the application without thinking about if it was loaded before.

Why is a data mapper a good extension to ZF? Because Data mapper is an enterprise pattern, and gets really useful in domain driven design. Zend Framework is an enterprise application framework. Other data mapper implementations in PHP are ezComponents Persistent Object (which leans to much to SQL Tables imho rather than on a Entity Class as the central datatype) and the under development FLOW3 and Doctrine 2.0 Frameworks.

The proposed data mapper solution will offer a generic solution and the possibility to extend certain parts of the mapper to match your needs perfectly. It will offer more flexibility for applications with complex business logic where a Table-Row-Data-Gateway will reach its limits faster. Using the DataMapper for an application as simple as a blog is nice, but rather overhead. This solution shines when you have more complex stuff going on.

This proposal is a first step, a second might be a proposal for *Zend_Entity_Repository* which implements the repository and specification patterns to completly encapsualte domain logic from persistence. Zend_Repository might use adapter to offer access to Zend_Db_Table instances, Zend_Db_Mapper persistence sessions and even an adapter for In Memory Repositories which would significantly enhance Unit Testing of Domain Logic. It could also include adapters for another current propsal Zend_CouchDb. Implementing the repository on top of a functional mapper is peanouts though :-)

Additional further support to enhance this component would be integration with a Database Schema Component and different Tooling providers.

h5. Clarification of terms

*Entity* An entity is a class/object that encapsulates Domain Logic. It uses all object-oriented reference types like composition, aggregation and such to represent the state of business objects in memory. It has onlly little relation with the relational entity definition.

*Data Mapper* A class that maps entities into a relational database based on mapping defintions. It does that behind an API that hides the relational database. In short words: It maps the memory representation of objects into the database.

*Entity Manager* Single point of entry for calls to the underlying mapping persistence engine. It controls the entity definitions, unit-of-work, the mapping engine and it allows CRUD operations on those entities via load, save and delete methods. These methods accept SQL through the Zend_Db_Select Query object (only).

*Collections* Sets of related entities are saved in collections. These are ArrayAccess and Iterator implementations. Implementing those as objects rather than simple PHP arrays allows for Lazy Loading of collections behind the scenes and offer a way for the data mapper to recognize which related objects have changed, are new or were deleted in a session.

*Repository* A repository completly hides the underlying persistence from the business domain, it does not allow for sql or query objects. It offers access to objects via Criteria objects that specifiy which objects may be retrieved. An extended repository allows saving and deleting of objects. Criteria are transformed into the concrete selecting language, SQL in the case of relational databases.

*Loader* Based on the entity metadata definitions a loader implementation is selected for each entity to load the data from the database in the most efficient way.

*Persister* Inverted principle of the loader, based on the definition the persister knows how to persist an entitiy and its related entities.

h5. Zend Package/Namespace Discussion

Some of the interfaces and implementations of this propsal are that generic, that an ORM based on for example CouchDb could also use them. The overlap is fairly small though and in general I think the use-case of migrating from CouchDb to a RDMS or the other way around is rather small.

For this proposal to be really useful in a generic context it should follow its primary terminology Entity for its frontline interfaces. The question is wheater separation into two namespaces "Zend_Entity" and "Zend_Db_Mapper" are required, or if everything goes into subpackages of Zend_Entity. Personally I would put it all under a new Zend_Entity namespace although that might be misleading.

What is definately required in my opinion is the definition of the following interfaces and subpackages in a new Zend_Entity package that other persistence layers might be using (to name a few CouchDB, XML Databases, Remote Webservice that manage models).

* Zend_Entity_Manager_Interface
* Zend_Entity_Interface
* Zend_Entity_Collection_Interface
* Zend_Entity_IdentityMap
* Zend_Entity_Query_*
* Zend_Entity_MetadataFactory_*
* Zend_Entity_LazyLoad_*

All the stuff that currently resides inside "Zend_Entity_Mapper_*" is database specific.

{zone-data}

{zone-data:references}
* [Martin Fowler: Data Mapper Pattern|http://martinfowler.com/eaaCatalog/dataMapper.html]
* [Martin Fowler: Unit Of Work|http://martinfowler.com/eaaCatalog/unitOfWork.html]
* [ezComponents Persistent Object|http://ezcomponents.org/docs/api/trunk/classtrees_PersistentObject.html]
* [FLOW 3 Persistence|http://flow3.typo3.org/documentation/reference/persistence]
* [Doctrine|http://www.doctrine-project.org]
* [Hibernate|http://www.hibernate.org]
* [Eric Evans: Domain Driven Design|http://www.domainlanguage.com/ddd/index.html]
{zone-data}

{zone-data:requirements}
* Zend_Db_Mapper *is NOT* a model proposal. It merely allows to save Entities (classes) persistent. See "Domain Model, Table Module or Transaction Script" Patterns in Fowler PEAA for a model.
* Zend_Db_Mapper *will* put the Entity (A Record Class) as Datastructure into the focus, not the where and how it is saved for persistance.
** An Entity is defined via a metadata syntax, by default a programmatic PHP implementation is shipped. Extension points for Annotations, XML or YAML configs are provided.
** Metadata allows to specifiy: Property Names, Column Names, Property Types for example and many other details regarding the mapping.
** Simple entity objects will be supported via an interface, which breaks encapsulation for PHP 5.2 objects
** For PHP 5.3 usage a Reflection API will be implemented that directly sets properties (even private and protected ones).
* Zend_Db_Mapper *will* lean to a Data Mapper like Hibernate, it will provide the most basic functionality but offer rich interfaces to extend it.
** It *will* take a Hibernate Mapping like syntax as basis to profit from the rich experience.
** It will have reasonable defaults, only deviations should be configured.
** It *will* provide a generic solution for the DataMapper pattern based on generic per entity data-mappers.
** It *will* provide full functionality to define relations between tables that are translated into object composition behind the scenes.
** Collections and object composition *will* make heavy use of behind the scences lazy loading to encapsulate object creation and domain logic without hurting performance.
** It *will* implement IdentityMap
** It *will not* implement UnitOfWork so far. It can be added as a decorator to the Entity Manager Session with hooks into the event API at a later point.
** It *will* provide a Query API for RDMS mappers that bases on Zend_Db_Select
** It *will* provide a Query Object API that is sql-likeish but speaks in terms of the domain model (properties, entities).
* Zend_Db_Mapper *will* provide extension points through modularity that allow to overwrite specific functionality of the mapper.
** It *will* strictly give responsibility of query building to subcomponents of the entity definitions such that developers can overwrite behaviour themselves (This compares to Doctrine behaviours)
** Collection and lazy load classes *can* be configured to deviate from their defaults.
** An event API *will* publish events on all entities.
{zone-data}

{zone-data:dependencies}
* Zend_Db_Adapter_Abstract
* Zend_Db_Select
* Zend_Loader_PluginLoader
* Zend_Loader_Autoloader_Resource
{zone-data}


{zone-data:operation}
*Steps of configuration:*
* For each object that is an Entity you have to create a definition file that describes the process of mapping this entity to the database.
* Each object that is an Entity has to be implemented. An interface can be implemented that breaks up encapsulation of the entity for the datamapper.
* The Entity Manager will have to be initialized to be aware of the definitions.

*Steps of a session:*
* A new *Entity Manager* is initializing with a Database connection and the mapping definitions.
* The *Entity Manager* is a single point of entry that encapsulates the *Mapping* and *Loading* of objects.
* The *Entity Manager* delegates all calls to the underlying concrete/generic mappers and the Query API.
* You can load Entities through the *Entity Manager* via Primary Key or a derived Zend_Db_Select object.
* The *Entity Manager* return and accept *Entity objects* that are controlled a very simple entity interface
* The *Entity Manager* delegates saving, selecting, updating and inserting.
* When *Entities* are retrieved the *Mapping* Engine decides how relations and large fields should be handled in termns of LazyLoading.
* *Entities* do not include any database related code (except hidden lazy loading mechanisms through an Inner/Outer Iterator schema).
* Besides more or less transactionless work, the session allows access to a Unit Of Work that handles a complete transaction based on the users need.

*Additional detail on my currenct concrete implementation proposal ideas:*

1.) Concrete Type Mapper vs Generic Mapper: It will be allowed to overwrite the lowlevel mapper for a particular entity. By default all entities will use a powerful generic entity mapper, so that only when performance issues occour it should be relevant to write hard-coded sql by overwriting a mapper.

2.) The *Entity Manager* keeps track of all dependencies such as Db Adapter, the unit of work, identity map, entity definitions and the underying mappers. They are enforced to be "singletons" with small "s" inside that session. This allows to have a hand on memory management which is necessary with UoW and Identity Map.

3.) LazyLoading will primarily be implemented at the Collection level, which under circumstances can lead to the N+1 query problem. This can be prevented by using outer joins which might be generated when loading the root object. You could also define a "formula" field can also be used to inject subselect values into an object field.

4.) Cascaded saving, updating and deleting of objects will be supported by configuration, allowing to save an entity and automatically saving all or some of its related entities.

5.) The generic implementation of the mapper is possible by using the Visitor pattern with the definition objects of a table. Depending on the state and action the mapper injects all the related data into a visitor accept function of the definition property. That way complicated lazy loading, association and other stuff is encapsulated at the point where its information is stored. This pattern allows for the great flexibility for developers to build their own properties with special handling that only need to be attached to the table definition and work without changes on the session and mapper code.

6.) As for every tool that tries to make you think less about the database this mapper implementation could cause considerable Database overhead, when used wrong. Therefore the documentation should by default include a section about pitfalls, performance and best-practices for special cases. To investigate the mapper behaviour I would like you to propose some object designs that we have to test in their persistent form.
{zone-data}

{zone-data:milestones}
* Milestone 1: Proposal (Done)
* Milestone 2: Early Prototype and add more Use-Cases from it (Done)
* Milestone 3: Reviews and Zend acceptance.
** Milestone 3a: Integrate community feedback (lots of that is already done, more to come)
** Milestone 3b: Integrate Zend feedback
* Milestone 4: Beta Phase with Iterative Feature Enhancements, Testing, Documentation and Collection of Scenarios
* Milestone 5: Release
{zone-data}

{zone-data:class-list}

Why two namespaces? I discuss some requirements of that in the Component Overview section.

Core classes (Public API)

* Zend_Entity_Manager_Interface
* Zend_Entity_IdentityMap
* Zend_Entity_Query_*
* Zend_Entity_MetadataFactory_*
* Zend_Entity_Manager_Interface

Entity and Collection classes
* Zend_Entity_Interface
* Zend_Entity_List (Simple list 1...n of related objects)
* Zend_Entity_Map (Objects accessiable via map-key => object association)
* Zend_Entity_Set (Unique objects)
* Zend_Entity_Collection_Interface
* Zend_Entity_LazyLoad_*

The namespace "Zend_Db_Mapper_" is currently "Zend_Entity_Mapper_*" in the svn and git repositories.

Database specific implementations of core interfaces
* Zend_Db_Mapper_EntityManager
* Zend_Db_Mapper
* Zend_Db_Mapper_Select
* Zend_Db_Mapper_Query_*

Loader and Persister classes
* Zend_Db_Mapper_Persister_Interface
* Zend_Db_Mapper_Loader_Interface
* A bunch of implementations based on the requirements of the entity definition

Definition classes (Tooling Providers would greatly enhance work with this)
* Zend_Db_Mapper_DefinitionMap
* Zend_Db_Mapper_Definition_Entity (*Core Definition*)
* Zend_Db_Mapper_Definition_Property (Simple column property mapped to entity field)
* Zend_Db_Mapper_Definition_PrimaryKey (Required Id field of entity)
* Zend_Db_Mapper_Definition_CompositeKey
* Zend_Db_Mapper_Definition_Formula (Read only properties that are SQL formulas, for example group formulas)
* Zend_Db_Mapper_Definition_Collection (Has Many collections of entities or value objects)
* Zend_Db_Mapper_Definition_Join (Join a second table that holds properties of entity)
* Zend_Db_Mapper_Definition_Component (Nested value object that is generated from table columns)
* Zend_Db_Mapper_Definition_Discriminator (Property that decides which sub-class to instantiate)
* Zend_Db_Mapper_Definition_Version (field to use for optimitistic locking)
* Zend_Db_Mapper_Definition_Timestamp (Update timestamp on saving)
* Zend_Db_Mapper_Definition_Date (Zend_Date Serializer)
* Zend_Db_Mapper_Definition_DateTime (DateTime Serializer)
* Zend_Db_Mapper_Definition_Id_Interface
* Zend_Db_Mapper_Definition_Id_AutoIncrement
* Zend_Db_Mapper_Definition_Id_Sequence
* Zend_Db_Mapper_Definition_Relation_Interface
* Zend_Db_Mapper_Definition_Relation_OneToMany
* Zend_Db_Mapper_Definition_Relation_ManyToOne
* Zend_Db_Mapper_Definition_Relation_OneToOne
* Zend_Db_Mapper_Definition_Relation_ManyToMany
{zone-data}


{zone-data:use-cases}


h5. Scenarios (Integration Tests)

I have implemented two scenarios for integration testing already, which are listed in the SVN repository:

http://framework.zend.com/svn/framework/standard/branches/user/beberlei/ZendEntity/tests/Zend/Entity/IntegrationTest

These show lots of use-cases and how the component works.

{deck:id=UseCaseBasic}
{card:label=Use-Case 1: Setup and simple usage}
Initialize db adapter and create the mapper factory with a plugin loader configuration.

{code}
$dbAdapter = Zend_Db::factory(..);
$manager = new Zend_Entity_Manager($dbAdapter, array(
'metadataFactory' => new Zend_Entity_MetadataFactory_Code('path')
));

$customer = $manager->load("Customer", 1);

$customer->name = "Foo";

$manager->save($customer);
{code}
{card}

{card:label=Use-Case 2: Usage with Zend_Db_Select}

We could also go to the concrete mapper managing an entity, both session and mapper offer acess via an extension of Zend_Db_Select objects,
which also implements the Zend_Paginator_Adapter_Interface

{code}
$select = $manager->createNativeQuery("Customer");
$select->where("customer_name = ?", "John Wayne");

$john = $select->getSingleResult();
$john->addOrder(...);
$manager->save($john);

// severals
$select = $manager->createNativeQuery("Customer");
$select->where("customer_country = ?", "Germany");

$customers = $select->getResultList();
{code}
{card}

{card:label=Use-Case 3: An Entity Class Implementation}

Our customer entity object used by the mappers before would look something like:

{code}
class Customer implements Zend_Entity_Interface
{
protected $id;

protected $name;

protected $salesHistory;

/**
* Required interface function for setting state of an object in PHP 5.2
*/
public function setState(array $state)
{
foreach($state AS $k => $v) {
$this->$k = $v;
}
}

/**
* Required function to get the state of an object in PHP 5.2
*/
public function getState()
{
return array(
'id' => $this->id,
'name' => $this->name,
'salesHistory' => $this->salesHistory, // Type Collection
);
}

/** Userland functions to retrieve and set properties */
public function getId()
{
return $this->id;
}

public function getName()
{
return $this->name;
}

public function setName($name)
{
$this->name = $name;
}

public function getSalesHistory()
{
return $this->salesHistory;
}

public function addSale(Sale $sale)
{
// salesHistory is a collection object
$this->salesHistory->add($sale);
}
}
{code}
{card}

{deck}

{deck:id=UseCaseDefinitions}
{card:label=Use-Case 4: Unit Of Work}

No Unit Of Work as of yet!
{card}


{card:label=Use-Case 5:Mapping Definition in PHP}

A mapper definition has to be generated for all entities that describe the details of the mapping scheme. The definitions will be lazyloaded the first time the mapper is used.

{code}
// You have to require and build this "Customer" class yourself implementing Zend_Entity_Interface as seen in UC-3
$def = new Zend_Db_Mapper_Definition_Entity("Customer");

// Sql Schema & Tablename
$def->setSchema("myApplicationDb");
$def->setTable("customers");

// In General: The add[$propertyType]($propertyName, $options) func uses the pluginloader to load definitions

// Add Primary Key
$def->addPrimaryKey("id", array(
"generator" => "AutoIncrement"
)
);

// Will load: Zend_Db_Mapper_Definition_Property
$def->addProperty("name", array("columnName" => "customer_name") );

// Retrieve all Sales and put them into salesHistory, by default all collections are lazy loaded.
$def->addCollection('salesHistory', array(
"table" => "CustomerSales", // Using a Join Table even for this OneToMany relation
"key" => "customer_id", // Key in Join table
// Define the relationtype and which property and DbColumn are responsible in the Sales Entity
"relation" => array("OneToMany", "id", array(
"columnName" => "id", // Could be set implicit by propertyName already
"class" => "Sale", // This relates to the entity "Class"
)),
)
);

// Retrieve the interests of the customer
$def->addCollection('interests', array(
"table" => "CustomerInterests", // Using a Join Table required for ManyToMany relationsships
"key" => "customer_id", // Key in Join table
// Define the relationtype and which property and DbColumn are responsible in the Interests Entity
"relation" => array("ManyToMany", "id", array(
"columnName" => "interest_id", // This is required for the join table
"class" => "Interest", // This relates to the entity "Class"
)),
"load" => "directly" // directly load these entites rather than waiting for lazy load trigger
)
);

return $def;
{code}
{card}

{card:label=A second Definition: Article}
An Blog Article that has, a User, a Category, joins statistical information from a second table using an Outer Join (optional = true) and loads the Many Information about Tags and Comments via Lazy Load collections.

{code}
// Constructor API: First Parameter proxies setClass(), the option array calls their setters.
$def = new Zend_Entity_Mapper_Definition_Entity("Article", array('table' => "articles"));
$def->addPrimaryKey("id", array(
'generator' => new Zend_Entity_Mapper_Definition_PrimaryKey_AutoIncrement(),
));

$def->addProperty("entry_type");
$def->addProperty("entry_headline");
$def->addProperty("entry_link");
$def->addProperty("entry_body");
$def->addProperty("entry_allowcomments");

$def->addProperty("created");

$def->addManyToOne("user", array(
"columnName" => "user_id",
"class" => "User",
"foreignKey" => "id",
"load" => "lazy",
));
$def->addManyToOne("category", array(
"columnName" => "category_id",
"class" => "Category",
"foreignKey" => "id",
"load" => "directly",
));

// Use a join table to gain certain statistical values that are aggregated into this
$join = $def->addJoin("article_statistics", array("key" => "article_id", "optional" => true));
$join->addFormula("comment_count", array(
'sqlExpr' => "(SELECT count(comments.id) FROM comments WHERE comments.article_id = articles.id)"
));
$join->addProperty("entry_hits");
$join->addProperty("article_update_count");

$def->addCollection("comments", array(
"table" => "comments",
"key" => "article_id",
"relation" => new WW_Entity_Mapper_Definition_Relation_OneToMany("id", array("class" => "Comment")),
"load" => "lazy",
));

$def->addCollection("tags", array(
"table" => "article2tag",
"key" => "article_id",
"cascade" => "all",
"load" => "lazy",
"relation" => new WW_Entity_Mapper_Definition_Relation_ManyToMany("tag", array("class" => "Tag")),
));

return $def;
{code}
{card}
{deck}

{zone-data}

{zone-data:skeletons}
{deck:id=Skeletons}

These skeletons describe the available Public API to the userland.

{card:label=Zend_Entity_Manager_Interface}
Generic interface for any Manager that works on entities. The only specific word
here is "select" which is kinda SQL but every other datasource has Query Objects
to which this method proxies.

{code}
interface Zend_Entity_Manager_Interface
{
/**
* Return concrete mapper implementation of the given Entity Type
*
* @param string|Zend_Entity_Interface $entity
* @return Zend_Entity_MapperInterface
*/
public function getMapperByEntity($entity);

/**
* @param string $entityName
* @return Zend_Entity_Query_AbstractQuery
*/
public function createNativeQuery($entityName);

/**
* @param string $entityName
* @return Zend_Entity_Query_AbstractQuery
*/
public function createQuery($entityName);

/**
* Find by primary key
*
* @param string $entityName
* @param string $key
* @return Zend_Entity_Interface
*/
public function load($entityName, $key);

/**
* Save entity by registering it with UnitOfWork or hitting the database mapper.
*
* @param Zend_Entity_Interface $entity
* @return void
*/
public function save(Zend_Entity_Interface $entity);

/**
* Try to delete entity by checking with UnitOfWork or directly going to mapper.
*
* @param Zend_Entity_Interface $entity
* @return void
*/
public function delete(Zend_Entity_Interface $entity);

/**
* Refresh object state from the database
*
* @param Zend_Entity_Interface $entity
* @return void
*/
public function refresh(Zend_Entity_Interface $entity);

/**
* Get a reference of an object.
*
* A reference is either a LazyLoad entity of the type {@see Zend_Entity_LazyLoad_Entity}
* or if the entity was loaded before and is found in the identity map the original is used.
*
* @param string $class
* @param int|string $id
*/
public function getReference($class, $id);

/**
* Check if entity instance belongs to the persistence context.
*
* @param Zend_Entity_Interface $entity
* @return boolean
*/
public function contains(Zend_Entity_Interface $entity);

/**
* Retrieve the underyling datasource adapter
*
* @return object
*/
public function getAdapter();

/**
* Tell Unit Of work to begin transaction
*
* @retun void
*/
public function beginTransaction();

/**
* Tell Unit of Work to commit transaction.
*/
public function commit();

/**
* Tell Unit of Work to rollback transaction.
*/
public function rollBack();

/**
* Clear persistence session, rolling back all current changes if transaction is open
* and deleting the UnitOfWork and Identity Map states.
*/
public function clear();

/**
* Close connection to database, commit transaction if any is open and call clear().
*/
public function close();

/**
* Retrieve Identity Map instance from EntityManager
*
* @return Zend_Entity_IdentityMap
*/
public function getIdentityMap();

/**
* @return Zend_Entity_MetadataFactory_Interface
*/
public function getMetadataFactory();
}
{code}
{card}

{card:label=Zend_Entity_Query_AbstractQuery}
{code}
abstract class Zend_Entity_Query_AbstractQuery implements Zend_Paginator_Adapter_Interface
{
abstract public function getResultList();

/**
*
* @throws Zend_Entity_Exception
* @return Zend_Entity_Interface
*/
public function getSingleResult();

abstract public function setFirstResult($offset);

abstract public function setMaxResults($itemCountPerPage);

/**
* @param int $offset
* @param int $itemCountPerPage
*/
public function getItems($offset, $itemCountPerPage);

abstract public function __toString();
}
{code}
{card}

{card:label=Zend_Entity_Interface}
{code}
interface Zend_Entity_Interface
{
/**
* Return the current COMPLETE state of the object as an array
*
* @return array
*/
public function getState();

/**

* Is used by Mappers to set complete or PARTIAL (!) information on an entity.
*
* @param array $state
* @return void
*/
public function setState(array $state);
}
{code}
{card}

{card:label=Zend_Entity_Collection}
The collection is required to be able to make a diff on commit and add, delete
only the relevant objects.

{code}
interface Zend_Entity_Collection extends ArrayAccess, Iterator, Countable
{
public function __construct(array $collection=array(), $entityClassType=null);

public function add($entity);

public function remove($index);

public function getDeleted();

public function getAdded();

public function wasLoaded();
}
{code}
{card}
{deck}
{zone-data}
{zone-template-instance}]]></ac:plain-text-body></ac:macro>