Plugin Support¶
Plugin Support
Plugin support is the main theme of VersionPress 4.0 which is currently in beta. Plugin developers, we'd like your feedback on this, feel free to open new issues or chat with us on Gitter.
VersionPress needs to understand plugin data, actions, shortcodes and other things to automatically provide version control for them. This document describes how plugins (and themes, later) can hook into VersionPress functionality.
Introduction¶
Plugins are described to VersionPress by a set of files stored in the .versionpress
folder in the plugin root (with other discovery options available, see below). They include:
actions.yml
– plugin actions, i.e., what the plugin doesschema.yml
– database schema (how the plugin stores data)shortcodes.yml
– shortcodeshooks.php
– other hooks
All files are optional so for example, if a plugin doesn't define any new shortcodes it can omit the shortcodes.yml
file. Simple plugins like Hello Dolly might even omit everything.
Tip
WordPress core is described using the very same format and you can find the definition files in the .versionpress
folder inside the plugin.
Actions¶
Actions represent what the plugin does. For example, WordPress core has actions like "update option", "publish post" and many others. They are the smallest changes in a WordPress site and are eventually stored as Git commits by VersionPress.
An action is identified by a string like option/edit
or post/publish
, commits some file(s) with it and has a human-readable message like "Updated option blogname", "Published post Hello World", etc.
Some commits may even contain multiple actions. For example, if a user switches to a new theme that also creates some options of its own, a single commit with theme/switch
and several option/create
actions will be created. When this operation is undone, it takes back both the theme switching and options creation.
Actions are described in the actions.yml
file.
actions.yml
¶
Here's an example from the WordPress core actions.yml
file:
post:
tags:
VP-Post-Title: post_title
VP-Post-Type: post_type
actions:
create: Created %VP-Post-Type% '%VP-Post-Title%'
edit:
message: Edited %VP-Post-Type% '%VP-Post-Title%'
priority: 12
postmeta:
tags:
VP-Post-Id: vp_post_id
VP-Post-Title: /
parent-id-tag: VP-Post-Id
actions:
...
theme:
tags:
VP-Theme-Name: /
actions:
install: Installed theme '%VP-Theme-Name%'
update: Updated theme '%VP-Theme-Name%'
These are the main elements:
- The top-level elements are scopes that basically group related actions together. For example, actions related to posts are in the
post
scope, theme actions are in thetheme
scope, etc. Scopes use a singular form. - Tags are values saved in commit messages and are typically used to make user-facing messages more useful. For example, it's better to display Created post 'Hello World' than Created post 123 and tags make this possible.
- Tags are either mapped to database fields as in the
post
example, or use the/
character to indicate that the value is provided by a filter (see below).
- Tags are either mapped to database fields as in the
- The
actions
section defines all actions of a scope.- An action has a message that can reference tags to make it more user-friendly. Messages use past tense.
- Each action has a priority – 10 by default. Priorities behave like on WordPress filters and actions: the lower the number, the higher the priority. A more important action beats the less important one if both appear in the same commit. For example,
theme/switch
beatsoption/edit
which means that the user will see a message about changing themes, not updating some internal option. - Priorities can be set dynamically using the
vp_action_priority_{$scope}
filter, see WPLANG handling as an example. - A combination of a scope and an action, e.g.,
post/create
ortheme/install
, uniquely identifies the action and can be searched for in the UI.
- An action has a message, usually in past tense, and a priority. If priority is not set, the default value of 10 is used.
- Priorities behave like on WordPress filters and actions: the lower the number, the higher the priority. A more important action beats the less important one if both appear in the same commit. For example,
theme/switch
beatsoption/edit
which means that the user will see a message about changing themes, not updating some internal option.
- Priorities behave like on WordPress filters and actions: the lower the number, the higher the priority. A more important action beats the less important one if both appear in the same commit. For example,
- Meta entities also contain
parent-id-tag
with the name of a tag containing ID of the parent entity.
Action detection¶
There are generally two types of actions:
- Database actions like manipulating posts, options, users, etc.
- Non-database actions like updating WordPress, deleting themes, etc.
Database actions are more common (at least in WordPress core) and get a pretty convenient treatment by default. Based on the SQL query issued, a create
, edit
or delete
action is created automatically.
If you need more specific actions like post/trash
or comment/approve
, filters are used: vp_entity_action_{$entityName}
for standard entities and vp_meta_entity_action_{$entityName}
for meta entities.
🚧 Hooks are not properly documented yet, please click through the hook names to at least browse the source codes on GitHub.
Tags are automatically extracted from the database entity. For example,
tags:
VP-Post-Title: post_title
makes sure that the message (defined as Created post '%VP-Post-Title%'
) automatically stores the real post title.
Tags can be altered (or created entirely if the YAML only uses /
as a tag value) by filters vp_entity_tags_{$entityName}
and vp_meta_entity_tags_{$entityName}
.
Non-database actions are tracked manually by calling a global vp_force_action()
function. This overwrites all other actions VersionPress might have collected during the request. For example, this is how wordpress/update
action is tracked:
vp_force_action('wordpress', 'update', $version, [], $wpFiles);
We're planning to change this for the final VersionPress 4.0 release. Some filter will probably be used instead.
Files to commit with an action¶
Every action has a message and some content. It's this content that is undone when the user clicks the Undo button in the UI.
For database actions, VersionPress automatically commits the corresponding INI file. For example, for a post/edit
action, a post's INI file is committed.
Side note: VersionPress stores database entities in the
wp-content/vpdb
folder as a set of INI files.
This behavior is sufficient most of the time, however, some changes should commit more files. For example, when the post is an attachment, the uploaded file should also be committed. For this, the list of files to commit can be filtered using the vp_entity_files_{$entityName}
or vp_meta_entity_files_{$entityName}
filters.
The array of files to commit can contain three different types of items:
Note
Concepts like VPIDs are explained in the "Database schema" section below.
-
Single file corresponding to an entity, for example:
[ 'type' => 'storage-file', 'entity' => 'post', 'id' => $vpid, 'parent-id' => $parentVpid // for meta entities ]
VersionPress automatically calculates the right path to the file.
-
All files of an entity type:
[ 'type' => 'all-storage-files', 'entity' => 'option' ]
-
Path on the filesystem:
[ 'type' => 'path', 'path' => 'some/path/supports/wildcards/*' ]
The full example might look something like this:
[
['type' => 'storage-file', 'entity' => 'post', 'id' => $vpid, 'parent-id' => null],
['type' => 'storage-file', 'entity' => 'usermeta', 'id' => $vpid, 'parent-id' => $userVpid],
['type' => 'all-storage-files', 'entity' => 'option'],
['type' => 'path', 'path' => '/var/www/wp/example.txt'],
['type' => 'path', 'path' => '/var/www/wp/folder/*']
]
For non-database actions, this list is one of the arguments of the vp_force_action()
function.
As noted above, we'll be getting rid of this approach so this is temporary info.
Database schema¶
If the plugin adds custom data into the database it must provide a schema.yml
file describing the database model. For example, this is how WordPress posts are described:
post:
table: posts
id: ID
references:
post_author: user
post_parent: post
mn-references:
term_relationships.term_taxonomy_id: term_taxonomy
ignored-entities:
- 'post_type: revision'
- 'post_status: auto-draft'
ignored-columns:
- comment_count: '@vp_fix_comments_count'
clean-cache:
- post: id
Defining entities¶
The top-level keys define entities such as post
, comment
, option
or postmeta
. Entity names use a singular form.
By default, entity names match database table names without the wp_
(or custom) prefix. It is possible to specify a different table using the table
property:
post:
table: posts
...
Again, this is prefix-less; wp_
or another prefix will be added automatically.
Identifying entities¶
VersionPress needs to know how to identify entities. There are two approaches and they are represented by either using a id
or vpid
property in the schema:
-
id
points to a standard WordPress auto-increment primary key. VersionPress will generate VPIDs (globally unique IDs) for such entities. Most entities are of this type – posts, comments, users etc. -
vpid
points VersionPress directly to use the given column as a unique identifier and skip the whole VPID generation and maintenance process. Entities of this type will not have artificial VPIDs. Theoptions
table is an example of this – even though it has anoption_id
auto-increment primary key, from VersionPress' point of view the unique identifier isoption_name
.
Examples:
post:
table: posts
id: ID
option:
table: options
vpid: option_name
References¶
VersionPress needs to understand relationships between entities so that it can update their IDs between environments. There are several types of references, each using a slightly different notation in the schema file.
Basic references¶
The most basic references are "foreign keys". For example:
post:
references:
post_author: user
post_parent: post
This says that the post_author
field points to a user while the post_parent
references another post.
Value references¶
Value references are used when a reference to an entity depends on another column value. For example, options might point to posts, terms or users and it will depend on which option it is. This is how it's encoded:
option:
value-references:
option_name@option_value:
page_on_front: post
default_category: term
...
This is the simplest case but it can also get more fancy:
If the entity type needs to be determined dynamically it can reference a PHP function:
postmeta:
value-references:
meta_key@meta_value:
_menu_item_object_id: '@\VersionPress\Database\VpidRepository::getMenuReference'
Note that there are no parenthesis at the end of this (it's a method reference, not a call) and that it is prefixed with @
. The function gets the entity as a parameter and returns a target entity name. For example, for _menu_item_object_id
, the function looks for a related DB row with _menu_item_type
and returns its value.
If the ID is in a serialized object, you can specify the path by a suffix of the source column. It looks like an array access but also supports regular expressions, for example:
option:
value-references:
option_name@option_value:
widget_pages[/\d+/]["exclude"]: post
To visualize this, the widget_pages
option contains a value like a:2:{i:2;a:3:{s:5:"title";s:0:"";s:7:"exclude";s:7:"1, 2, 3";...}...}
which, unserialized, looks like this:
[
2 => [
"title" => "",
"sortby" => "post_title",
"exclude" => "1, 2, 3"
],
"_multiwidget" => 1
]
The schema says that the numbers in the "exclude" key reference posts.
Value references also support wildcards in the name of the source column. It's useful e.g. for options named theme_mods_<name of theme>
. An example that mixes this with the serialized data syntax is:
option:
value-references:
option_name@option_value:
theme_mods_*["nav_menu_locations"][/.*/]: term
theme_mods_*["header_image_data"]["attachment_id"]: post
theme_mods_*["custom_logo"]: post
It probably won't surprise you that this is a real example used in WordPress' schema.yml
.
Another supported feature are IDs in serialized data in serialized data (really).
An example from WooCommerce: a:1:{s:4:"cart";s:99:"a:1:{s:32:"a5bfc9e07964f8dddeb95fc584cd965d";a:2:{s:10:"product_id";i:37;s:12:"variation_id";i:0;}}";}
.
session:
value-references:
session_key@session_value:
"*[\"cart\"]..[/.*/][\"product_id\"]": product
"*[\"cart\"]..[/.*/][\"variation_id\"]": variation
The complete syntax is:
value-references:
<source_column_name>@<value_column_name>:
<source_column_value>: <foreign_entity_name | @mapping_function>
<source_column_value>["path-in-serialized-objects"][/\d+/][0]..["key-in-nested-serialized-array"]: <foreign_entity_name | @mapping_function>
<columns_with_prefix_*>: <foreign_entity_name | @mapping_function>
M:N references¶
Some entities are in an M:N relationship like posts and term_taxonomies. This is how it's written:
post:
mn-references:
term_relationships.term_taxonomy_id: term_taxonomy
One entity is considered the main one which is kind of arbitrary as technically, VersionPress treats them equally. Here, we decided that posts will store tags and categories in them, not the other way around.
The syntax is:
mn-references:
<junction_table_name_without_prefix>.<column_name>: <foreign_entity_name>
References can also be prefixed with a tilde (~
) which makes them "virtual":
mn-references:
~<junction_table_name_without_prefix>.<column_name>: <foreign_entity_name>
A virtual reference is not stored in the INI file but the relationships are still checked during reverts. For example, when a revert would delete a category (revert of term_taxonomy/create
) and there is some post referencing it, the operation would fail. This is ensured by:
term_taxonomy:
mn-references:
~term_relationships.object_id: post
Parent references¶
Some entities are stored within other entities, for example, postmeta are stored in the same INI file as their parent post. This is captured using a parent-reference
property:
postmeta:
parent-reference: post_id
references:
post_id: post
This references one of the basic reference column names, not the final entity. The notation above reads "postmeta stores a parent reference in the post_id
column, and that points to the post
entity".
Frequently written entities¶
Some entities are changed very often, e.g., view counters, Akismet spam count, etc. VersionPress only saves them once in a while and the frequently-written
section influences this:
entity:
frequently-written:
- 'column_name: value'
- query: 'column1_name: value1 column2_name: value2'
interval: 5min
The values in the frequently-written
array can either be strings which are then interpreted as queries, or objects with query
and interval
keys.
- Queries identify entities via this syntax.
- The interval is parsed by the
strtotime()
function and the default value is one hour.
Ignoring entities¶
Some entities should be ignored (not tracked at all) like transient options, environment-specific options, etc. Queries are used again:
ignored-entities:
- 'option_name: _transient_*'
- 'option_name: _site_transient_*'
- 'option_name: siteurl'
Ignoring columns¶
It is possible to ignore just parts of entities. The columns might either be ignored entirely or computed dynamically using a PHP function:
entity:
ignored-columns:
- column_name_1
- column_name_2
- computed_column_name: '@functionReference'
The function is called whenever VersionPress does its INI files => DB synchronization. The function will get an instance of VersionPress\Database\Database
as an argument and is expected to update the database appropriately. The Database
class has the same methods as wpdb
but the changes it make are not tracked by VersionPress itself.
Cache invalidation¶
WordPress uses cache for posts, comments, users, terms, etc. This cache needs to be invalidated when VersionPress updates database (on undo, rollback, pull, etc.). It is possible to tell VersionPress which cache to invalidate and where to find the related IDs.
For example, when some post is deleted using the Undo functionality, it is necessary to call clean_post_cache(<post-id>)
. VersionPress will do it automatically based on following piece of schema:
post:
table: posts
id: ID
clean-cache:
- post: id
It tells VersionPress to delete the post cache (VP resolves the function name as clean_<cache-type>_cache
). You can use id
as the source of IDs for invalidation or a reference. For example like this:
post:
references:
post_author: user
post_parent: post
clean-cache:
- post: id
- user: post_author
- post: post_parent
Shortcodes¶
Similarly to database schema, VersionPress needs to understand shortcodes as they can also contain entity references. shortcodes.yml
describes this, here is an example:
shortcode-locations:
post:
- post_content
shortcodes:
gallery:
id: post
ids: post
include:
exclude: post
playlist:
id: post
ids: post
include: post
exclude: post
The shortcode-locations
array tells VersionPress where the shortcodes can appear. By default, WordPress only allows shortcodes in post content but here's an example of how it could look if it also supported them in post titles and comments:
shortcode-locations:
post:
- post_content
- post_title
comment:
- comment_content
Note that WordPress doesn't restrict shortcode type for various locations, so if some shortcode is supported in e.g. comment_content
, all shortcodes are.
The shortcodes
array holds the actual shortcodes, but only those that contain references to other entities so things like [embed]
or [audio]
are not present. Here's an example:
shortcodes:
gallery:
id: post
ids: post
include:
exclude: post
playlist:
id: post
ids: post
include: post
exclude: post
For example the [gallery]
shortcode has four attributes that can contain references, and they all point to the post
entity (it's an entity, not a table; the table will eventually be something like wp_posts
).
Note that you don't have to worry about the attribute type, whether it contains a single ID or a list of IDs. VersionPress handles both cases automatically:
[gallery id="1"]
[gallery id="1,2,3,6,11,20"]
Hooks¶
If something cannot be described statically, VersionPress offers several filters, actions and functions to define behavior through code. Implement them in the hooks.php
file.
Most of the filters have already been discussed in the text above, you can find the full API reference below.
Ignored folders¶
Feel free to use custom .gitignore
for files in the plugin directory. You can also ignore files / directories outside the plugin directory. There will be a filter to let VersionPress know which files / directories you want to ignore.
Discovery mechanism¶
VersionPress looks for plugin definitions in these locations, in this order:
WP_CONTENT_DIR/.versionpress/plugins/<plugin-slug>
(user-editable definitions in thewp-content
directory)WP_PLUGIN_DIR/<plugin-slug>/.versionpress
(definitions bundled with plugins)
The first definition found is used.
Resources¶
- Issue #1036 – everything was discussed there.
API reference¶
TODO this will be auto-generated from code.
Filters¶
vp_entity_action_{$entityName}
apply_filters("vp_entity_action_{$entityName}", $action, $oldEntity, $newEntity)
vp_meta_entity_action_{$entityName}
apply_filters("vp_meta_entity_action_{$entityName}", $action, $oldEntity, $newEntity, $oldParentEntity, $newParentEntity)
vp_entity_tags_{$entityName}
apply_filters("vp_entity_tags_{$entityName}", $tags, $oldEntity, $newEntity, $action)
vp_meta_entity_tags_{$entityName}
apply_filters("vp_meta_entity_tags_{$entityName}", $tags, $oldEntity, $newEntity, $action, $oldParentEntity, $newParentEntity)
vp_entity_files_{$entityName}
apply_filters("vp_entity_files_{$entityName}", $files, $oldEntity, $newEntity)
vp_meta_entity_files_{$entityName}
apply_filters("vp_meta_entity_files_{$entityName}", $files, $oldEntity, $newEntity, $oldParentEntity, $newParentEntity)
vp_entity_should_be_saved_{$entityName}
apply_filters("vp_entity_should_be_saved_{$entityName}", $shouldBeSaved, $data, $storage)
vp_bulk_change_description_{$entityName}
apply_filters("vp_bulk_change_description_{$entityName}", $description, $action, $count, $tags)
vp_action_description_{$scope}
apply_filters("vp_action_description_{$scope}", $message, $action, $vpid, $tags)
vp_action_priority_{$scope}
apply_filters("vp_action_priority_{$entityName}", $defaultPriority, $action, $vpid, $entity)
apply_filters("vp_action_priority_{$entityName}", $defaultPriority, $action, $vpid)
Actions (API)¶
vp_before_synchronization_{$entityName}
do_action("vp_before_synchronization_{$entityName}")
vp_after_synchronization_{$entityName}
do_action("vp_after_synchronization_{$entityName}")
Functions¶
vp_force_action
vp_force_action($scope, $action, $id = '', $tags = [], $files = [])
Query syntax¶
Ignored and frequently written entities are identified using small queries that look like this:
frequently-written:
- 'option_name: akismet_spam_count'
ignored-entities:
- 'option_name: _transient_*'
- 'option_name: cron'
- ...
The syntax is generally shared with the commit search:
- Everything is case insensitive.
field: value
is equivalent tofield: VALUE
orFiELd: vaLUE
.- One caveat is that your database might be configured to use case sensitive comparisons, in which case you'll need to be precise. (WordPress uses the case insensitive
utf8_general_ci
collation by default.)
- One caveat is that your database might be configured to use case sensitive comparisons, in which case you'll need to be precise. (WordPress uses the case insensitive
- Wildcards are supported.
- Multiple fields can be used, for example,
field1:value field2:value
.
Unlike the full search, you can only target entity fields, not things like commit authors, date ranges, etc.