tainacan/docs/internal-api.md

14 KiB

Tainacan Internal API

This page shows how the internal API works and how to create and fetch all kinds of entities in Tainacan: Collections, items, taxnomies, fields, filters, terms, item metadata and logs.

Its important that you first get familiar with the key concepts of tainacan.

This page gives an overview of the API. Detailed documentation and reference on each entity can be found below:

Overview

Tainacan adds a tiny layer of abstraction over WordPress to handle all its entities, but at the end of the day, everything is stored as a post of a specific post type (except terms). So for someone used to the way WordPress works, the data structure have no misteries at all.

This layer is based on a Repository pattern. Each entity Tainacan deals with have a correspondent Repository class and a correspondent Entity class.

Repositories are the classes that comunicate with the database and know where everything is stored and how to find things. It is a singleton class, so it have only one instance available to be used by any part of the application.

$fields_repo = Tainacan\Repositories\Fields::get_instance();

Entities classes are the representation of the individual of each repository.

This abstraction allows us to easily manipulate entities without worrying how to save or fetch them.

For example, Fields have many attributes, such as name and required (indicating wether this field is required or not). As mentioned before, Fields are stored as posts of a special post type called tainacan-field. The name of the field is, of course, the post_title and this required attribute is stored as a post_meta.

However, you dont need to bother about that. This pattern allows you to manipulate a Field entity and it attributes in a transparent way, such as:

$field->get_name(); // returns the field name
$field->get_required(); // returns the required value 
$field->set_name('new name');
$field->set_required('yes');

Tainacan will automatically map the values of the attributes to and from where they are stored.

When you want to fetch entities from the database, this abstraction layer steps aside and let you use all the power and flexibility of WP_Query, which you know and love. For example:

Repositories\Fields::get_instance()->fetch('s=test');

The fetch method from the repositories accept exactly the same arguments accepted by WP_Query and uses it internally. In fact, you could use WP_Query directly if you prefer, but using the repository class gives you some advantages. You dont have to know the name of the post type, you can also fetch by some mapped attribute calling it directly, withour having to use meta_query (or even know wether a property is stored as a post attribute or post_meta). See more details in the Fetch section below.

Fetching data

Every repository have a fetch() method to fetch data from the database. Some repositories may have other fetch methods, such as fetch_by_collection, please refer to their reference to find out.

Getting one single individual

If you have the ID or the WP_Post object, you can get the item by initializing a new instance of the Entity class:

$collection = new Tainacan\Entities\Collection($collection_post);
$collection = new Tainacan\Entities\Collection($collection_id);

This will have the same effect as calling the fetch method from the repository passing an integer as argument. THe repository will assume it is the collection ID.

$collection = Tainacan\Repositories\Collections::get_instance()->fetch($collection_id);

Fethcing many individuals

To fetch collections (or any other entity) based on a query search, you may call the fetch method from the repository and use any paramater of the WP_Query class.

the only exception for this are terms, which are saved as WordPress terms and gets paramaters from the get_terms() function instead

Examples:

$repo = Tainacan\Repositories\Collections::get_instance();
$items_repo = Tainacan\Repositories\Collections::get_instance();


$collections = $repo->fetch(); // get all public collections (or any private collections current user can view. It works exactly the same way WP_Query)

/**
 * fetch all items with title equal to 'test'
 */
$items = $items_repo->fetch([
	'post_title' => 'test'
]);

/**
 * fetch all items with title equal to 'test'
 * but using the mapped property name instead of the real name
 */
$items = $items_repo->fetch([
	'title' => 'test'
]);

Note that you can use the mapped attribute names to build your query, but it is just fine if you want to use the native WordPress names. The same can be achievied with attributes stored as post_meta:

$repo = Tainacan\Repositories\Fields::get_instance();

$fields = $repo->fetch([
	'required' => 'yes'
]);

// is the same as

$fields = $repo->fetch([
	'meta_query' => [
		[
			'key' => 'required',
			'value' => 'yes'
		]
	]
]);

Output

Fetch methods accept an attribute to choose how you want your output.

By default, it is a WP_Query object, which you can use to build your loop just as if you had called WP_Query your self.

But it also can be an array of Taincan Entities objects. This is very useful when you want to manipulate them.

Inserting

All repositories have a insert() method that gets an Entity as argument and save it in the database. If the entity has an ID, this method will update the entity. (yes, the same way wp_insert_post works)

Each repository will get as a parameter an instace of its correspondent entity. For example, Collections repository insert() will get an instace of Tainacan\Entities\Collection and return the updated entity.

However, before insertion, you must validate the entity, calling the validate() method every entity has. You can only insert valid entities.

So this is a typical routine for creating an entity:

$collectionsRepo = \Tainacan\Repositories\Collections::get_instance();
$collection = new \Tainacan\Entities\Collection();
$collection->set_name('New Collection');

if ($collection->validate()) {
	$insertedCollection = $collectionsRepo->insert($collection);
	echo 'Now I have an ID! ' . $insertedCollection->get_id();
	
	// Lets update something
	$insertedCollection->set_description('new description');
	if ($insertedCollection->validate()) {
		$insertedCollection = $collectionsRepo->insert($insertedCollection);
		echo 'I still have the same ID! ' . $insertedCollection->get_id();
	} else {
		$errors = $insertedCollection->get_errors();
	}
	
} else {
	$validationErrors = $collection->get_errors();
	// Do something!
}

IMPORTANT: Repositories insert() methods do not check for user permissions. If you call it, it will save entities to the database no matter who is logged in (or even if some is logged in). Again, this works the same way WordPress works with its internal functions. All permission checks must be done before you call the insertion methods. See "checking for permissions" section below.

The example above shows how to create and update a Collection, but it applies to every entity. They all work in the very same way.

Well, Item Metadata Entity is slightly different.

Handling Item Metadata

Item Metada is a special kind of entity, because it is not an actual entity itself. Rather, it is the relationship between an Item and a Field. And this relationship has a value.

So imagine a Collection of pens has a Field called "color". This means the an item of this collection will have a relationship with this field, and this relation will have a value. Red, for example.

So the Item Metadata Entity constructor gets to entities: an item and a field. Lets see an example, considering I alredy have a collection with fields and an item.

// Considering $item is an existing Item Entity an $field an existing Field Entity
$itemMetadada = new \Tainacan\Entities\ItemMetadataEntity($item, $field);

$itemMetadata->set_value('Red');

if ($itemMetadata->validate()) {
	$ItemMetadataRepo = \Tainacan\Repositories\ItemMetadata::get_instance();
	$ItemMetadata = $ItemMetadataRepo->insert($ItemMetadata);
} else {
	$errors = $ItemMetadata->get_errors();
}

Note: "Multiple" Fields, which can have more than one value for the same item, work exactly the same way, with the difference that its value is an array of values, and not just one single value.

If you want to iterate over all fields of an item or a collection, there are 2 usefull methods you can use. Fields repository have a fetch_by_collection() method that will fetch all fields from a given collection and return them in the right order.

Also, ItemMetadata Repository fetch() method will return an array of ItemMetadata Entities related to a given item.

Handling Compound fields

Compound fields are a special type of fields that support child fields. It is a group of fields.

The Compound field itself does not have a value, only its children have. So when you are saving a new value for a child field of a compound field, it will behave as it was a normal field.

However, when you save the value for the second field of that same group, you must inform that it belong to that group. You do this by passing a parent_meta_id when initializing the Item Metada Entity. Note that you will only have this ID after you saved the first ItemMetadata of that group, because only then the group was created.


$item_metadata1 = new \Tainacan\Entities\Item_Metadata_Entity($i, $child_field1);
$item_metadata1->set_value('Red');

if ($item_metadata1->validate()) {
	$item_metadata1 = $Tainacan_Item_Metadata->insert($item_metadata1);
}

$item_metadata2 = new \Tainacan\Entities\Item_Metadata_Entity($i, $child_field2, null, $item_metadata1->get_parent_meta_id());
$item_metadata2->set_value('Blue');

if ($item_metadata2->validate()) {
	$item_metadata2 = $Tainacan_Item_Metadata->insert($item_metadata2);
}


Now you may get the value directly from the Compound Field, and it will return an array of ItemMetadata Entities (with meta_id and parent_meta_id populated).


$compoundItemMeta = new \Tainacan\Entities\Item_Metadata_Entity($item, $compoundField);

$compoundValue = $compoundItemMeta->get_value();

// This is an array of ItemMetadata Entities

foreach ($compoundValue as $field_id => $childItemMeta) {
	var_dump( $childItemMeta instanceof \Tainacan\Entities\ItemMetadataEntity ); // true
	var_dump( $field_id == $childItemMeta->get_field()->get_id() ); // true
	echo "Value for field " . $childItemMeta->get_field()->get_name() " (child of " . $compoundItemMeta->get_name() . ") is:" . $childItemMeta->get_value();
	var_dump( $childItemMeta->get_field()->get_parent() == compoundItemMeta->get_field()->get_id() ); // true
	var_dump( is_int($childItemMeta->get_meta_id()) && $childItemMeta->get_parent_meta_id() ); // true. they are allways set when initialized by calling get_value() on the parent ItemMetadataEntity
}


More about validating

TODO: document the validation chains

All validations validate the property with the validation declared in the get_map() method of the repository.

Validate item -> call ItemMetadata->validate() for each field

Validate ItemMetadata -> call $fieldType->validate() for the Field type of the field.

Validate Field -> call validate_options() of the Field type

Checking for permissions

Each entity type is stored as a post type and has its own set of capabilities. For example, Collections are posts of the tainacan-collection post type, and have associated capabilities such as edit_tainacan-collections and edit_others_tainacan-collections.

If you are familiar with WordPress Roles and capabilities and, more specifically, with custom post types capabilities, this is very easy to understand. If you are not, its best if you first learn how WordPress handles custom post types capabilities and you will easily understand how tainacan works with it.

To see a complete list of tainacan capabilities see Tainacan Permissions.

When you use WordPress custom post types, you dont need to know the exact name of the capabilities of a post type to check for them. The post type object has a property called cap that informs you what are the specific capabilities that the post type have for the post type actions.

For example, for a post type called book, that have capabilities such as edit_books, you could:

if (current_user_can('edit_books')) 
	// do something

OR

$book_cpt = get_post_type_object('book');
if (current_user_can( $book_cpt->cap->edit_posts )) 
	// do something

This makes life easier and Tainacan works exacty the same way.


$collection = new \Tainacan\Entities\Collection(23);

var_dump( $collection->get_capabilities() ); // all capabilities of the collections post type

This is specially usefull when handling Items, because they are posts of dynamic created post types, and it would cost too much to find out the correct capabilties names.

Also, every entity implement 3 methods to check for the Meta Capabilities edit_post, delete_post and read_post, so intead of:

current_user_can( $item->get_capabilities()->edit_post, $item->get_id() ); 

You can simply

$item->can_edit(); 

So now you know how to check the permision when a user wants to update an item. Here is the complete code:

$collectionsRepo = \Tainacan\Repositories\Collections::get_instance();
$collection = new \Tainacan\Entities\Collection(23);

if ($collection->can_edit()) {
	
	$collection->set_description('new description');
	
	if ($collection->validate()) {
		
		$collection = $collectionsRepo->insert($collection);
		
	} else {
		$validationErrors = $collection->get_errors();
		// Do something!
	}
} else {
	echo 'Sorry, you dont have permission to do that';
}