## Example: Two Text Form Fields
-As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the [first part of the tutorial series](../../tutorial/series/part-1.md) will be updated to the new form builder API.
+As the first example, the pre-WoltLab Suite Core 5.2 versions of the forms to add and edit persons from the [first part of the tutorial series](../../tutorial/series/part_1.md) will be updated to the new form builder API.
This form is the perfect first examples as it is very simple with only two text fields whose only restriction is that they have to be filled out and that their values may not be longer than 255 characters each.
As a reminder, here are the two relevant PHP files and the relevant template file:
We will start this tutorial series by creating a base structure for the package and then continue by adding further features step by step using different APIs.
Note that in the context of this example, not every added feature might make perfect sense but the goal of this tutorial is not to create a useful package but to introduce you to WoltLab Suite.
-- [Part 1: Base Structure](part-1.md)
-- [Part 2: Event Listeners and Template Listeners](part-2.md)
-- [Part 3: Person Page and Comments](part-3.md)
+- [Part 1: Base Structure](part_1.md)
+- [Part 2: Event Listeners and Template Listeners](part_2.md)
+- [Part 3: Person Page and Comments](part_3.md)
+++ /dev/null
-# Tutorial Series Part 1: Base Structure
-
-In the first part of this tutorial series, we will lay out what the basic version of package should be able to do and how to implement these functions.
-
-
-## Package Functionality
-
-The package should provide the following possibilities/functions:
-
-- Sortable list of all people in the ACP
-- Ability to add, edit and delete people in the ACP
-- Restrict the ability to add, edit and delete people (in short: manage people) in the ACP
-- Sortable list of all people in the front end
-
-
-## Used Components
-
-We will use the following package installation plugins:
-
-- [acpTemplate package installation plugin](../../package/pip/acp-template.md),
-- [acpMenu package installation plugin](../../package/pip/acp-menu.md),
-- [file package installation plugin](../../package/pip/file.md),
-- [language package installation plugin](../../package/pip/language.md),
-- [menuItem package installation plugin](../../package/pip/menu-item.md),
-- [page package installation plugin](../../package/pip/page.md),
-- [sql package installation plugin](../../package/pip/sql.md),
-- [template package installation plugin](../../package/pip/template.md),
-- [userGroupOption package installation plugin](../../package/pip/user-group-option.md),
-
-use [database objects](../../php/database-objects.md), create [pages](../../php/pages.md) and use [templates](../../view/templates.md).
-
-
-## Package Structure
-
-The package will have the following file structure:
-
-```
-├── acpMenu.xml
-├── acptemplates
-│ ├── personAdd.tpl
-│ └── personList.tpl
-├── files
-│ └── lib
-│ ├── acp
-│ │ ├── form
-│ │ │ ├── PersonAddForm.class.php
-│ │ │ └── PersonEditForm.class.php
-│ │ └── page
-│ │ └── PersonListPage.class.php
-│ ├── data
-│ │ └── person
-│ │ ├── PersonAction.class.php
-│ │ ├── Person.class.php
-│ │ ├── PersonEditor.class.php
-│ │ └── PersonList.class.php
-│ └── page
-│ └── PersonListPage.class.php
-├── install.sql
-├── language
-│ ├── de.xml
-│ └── en.xml
-├── menuItem.xml
-├── package.xml
-├── page.xml
-├── templates
-│ └── personList.tpl
-└── userGroupOption.xml
-```
-
-
-## Person Modeling
-
-### Database Table
-
-As the first step, we have to model the people we want to manage with this package.
-As this is only an introductory tutorial, we will keep things simple and only consider the first and last name of a person.
-Thus, the database table we will store the people in only contains three columns:
-
-1. `personID` is the unique numeric identifier of each person created,
-1. `firstName` contains the first name of the person,
-1. `lastName` contains the last name of the person.
-
-The first file for our package is the `install.sql` file used to create such a database table during package installation:
-
-```sql
---8<-- "tutorial/tutorial-series/part-1/install.sql"
-```
-
-### Database Object
-
-#### `Person`
-
-In our PHP code, each person will be represented by an object of the following class:
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php"
-```
-
-The important thing here is that `Person` extends `DatabaseObject`.
-Additionally, we implement the `IRouteController` interface, which allows us to use `Person` objects to create links, and we implement PHP's magic [__toString()](https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring) method for convenience.
-
-For every database object, you need to implement three additional classes:
-an action class, an editor class and a list class.
-
-#### `PersonAction`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php"
-```
-
-This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
-This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
-`$permissionsDelete` has to be set to the permission needed in order to delete a person.
-We will later use the [userGroupOption package installation plugin](../../package/pip/user-group-option.md) to create the `admin.content.canManagePeople` permission.
-`$requireACP` restricts deletion of people to the ACP.
-
-#### `PersonEditor`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php"
-```
-
-This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
-setting the static `$baseClass` property to the database object class name.
-
-#### `PersonList`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php"
-```
-
-Due to the default implementation of `DatabaseObjectList`, our `PersonList` class just needs to extend it and everything else is either automatically set by the code of `DatabaseObjectList` or, in the case of properties and methods, provided by that class.
-
-
-## ACP
-
-Next, we will take care of the controllers and views for the ACP.
-In total, we need three each:
-
-1. page to list people,
-1. form to add people, and
-1. form to edit people.
-
-Before we create the controllers and views, let us first create the menu items for the pages in the ACP menu.
-
-### ACP Menu
-
-We need to create three menu items:
-
-1. a “parent” menu item on the second level of the ACP menu item tree,
-1. a third level menu item for the people list page, and
-1. a fourth level menu item for the form to add new people.
-
-```xml
---8<-- "tutorial/tutorial-series/part-1/acpMenu.xml"
-```
-
-We choose `wcf.acp.menu.link.content` as the parent menu item for the first menu item `wcf.acp.menu.link.person` because the people we are managing is just one form of content.
-The fourth level menu item `wcf.acp.menu.link.person.add` will only be shown as an icon and thus needs an additional element `icon` which takes a FontAwesome icon class as value.
-
-### People List
-
-To list the people in the ACP, we need a `PersonListPage` class and a `personList` template.
-
-#### `PersonListPage`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php"
-```
-
-As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
-
-1. We need to set the active ACP menu item via the `$activeMenuItem`.
-1. `$neededPermissions` contains a list of permissions of which the user needs to have at least one in order to see the person list.
- We use the same permission for both the menu item and the page.
-1. The database object list class whose name is provided via `$objectListClassName` and that handles fetching the people from database is the `PersonList` class, which we have already created.
-1. To validate the sort field passed with the request, we set `$validSortFields` to the available database table columns.
-
-#### `personList.tpl`
-
-```smarty
---8<-- "tutorial/tutorial-series/part-1/acptemplates/personList.tpl"
-```
-
-We will go piece by piece through the template code:
-
-1. We include the `header` template and set the page title `wcf.acp.person.list`.
- You have to include this template for every page!
-1. We set the content header and additional provide a button to create a new person in the content header navigation.
-1. As not all people are listed on the same page if many people have been created, we need a pagination for which we use the `pages` template plugin.
- The `{hascontent}{content}{/content}{/hascontent}` construct ensures the `.paginationTop` element is only shown if the `pages` template plugin has a return value, thus if a pagination is necessary.
-1. Now comes the main part of the page, the list of the people, which will only be displayed if any people exist.
- Otherwise, an info box is displayed using the generic `wcf.global.noItems` language item.
- The `$objects` template variable is automatically assigned by `wcf\page\MultipleLinkPage` and contains the `PersonList` object used to read the people from database.
-
- The table itself consists of a `thead` and a `tbody` element and is extendable with more columns using the template events `columnHeads` and `columns`.
- In general, every table should provide these events.
- The default structure of a table is used here so that the first column of the content rows contains icons to edit and to delete the row (and provides another standard event `rowButtons`) and that the second column contains the ID of the person.
- The table can be sorted by clicking on the head of each column.
- The used variables `$sortField` and `$sortOrder` are automatically assigned to the template by `SortablePage`.
-1. The `.contentFooter` element is only shown if people exist as it basically repeats the `.contentHeaderNavigation` and `.paginationTop` element.
-1. The JavaScript code here fulfills two duties:
- Handling clicks on the delete icons and forwarding the requests via AJAX to the `PersonAction` class, and setting up some code that triggers if all people shown on the current page are deleted via JavaScript to either reload the page or show the `wcf.global.noItems` info box.
-1. Lastly, the `footer` template is included that terminates the page.
- You also have to include this template for every page!
-
-Now, we have finished the page to manage the people so that we can move on to the forms with which we actually create and edit the people.
-
-### Person Add Form
-
-Like the person list, the form to add new people requires a controller class and a template.
-
-#### `PersonAddForm`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php"
-```
-
-The properties here consist of two types:
-the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the “data” properties `$firstName` and `$lastName`, which will contain the data entered by the user of the person to be created.
-
-Now, let's go through each method in execution order:
-
-1. `readFormParameters()` is called after the form has been submitted and reads the entered first and last name and sanitizes the values by calling `StringUtil::trim()`.
-1. `validate()` is called after the form has been submitted and is used to validate the input data.
- In case of invalid data, the method is expected to throw a `UserInputException`.
- Here, the validation for first and last name is the same and quite basic:
- We check that any name has been entered and that it is not longer than the database table column permits.
-1. `save()` is called after the form has been submitted and the entered data has been validated and it creates the new person via `PersonAction`.
- Please note that we do not just pass the first and last name to the action object but merge them with the `$this->additionalFields` array which can be used by event listeners of plugins to add additional data.
- After creating the object, the `saved()` method is called which fires an event for plugins and the data properties are cleared so that the input fields on the page are empty so that another new person can be created.
- Lastly, a `success` variable is assigned to the template which will show a message that the person has been successfully created.
-1. `assignVariables()` assigns the values of the “data” properties to the template and additionally assigns an `action` variable.
- This `action` variable will be used in the template to distinguish between adding a new person and editing an existing person so that which minimal adjustments, we can use the template for both cases.
-
-#### `personAdd.tpl`
-
-```smarty
---8<-- "tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl"
-```
-
-We will now only concentrate on the new parts compared to `personList.tpl`:
-
-1. We use the `$action` variable to distinguish between the languages items used for adding a person and for creating a person.
-1. Including the `formError` template automatically shows an error message if the validation failed.
-1. The `.success` element is shown after successful saving the data and, again, shows different a text depending on the executed action.
-1. The main part is the `form` element which has a common structure you will find in many forms in WoltLab Suite Core.
- The notable parts here are:
- - The `action` attribute of the `form` element is set depending on which controller will handle the request.
- In the link for the edit controller, we can now simply pass the edited `Person` object directly as the `Person` class implements the `IRouteController` interface.
- - The field that caused the validation error can be accessed via `$errorField`.
- - The type of the validation error can be accessed via `$errorType`.
- For an empty input field, we show the generic `wcf.global.form.error.empty` language item.
- In all other cases, we use the error type to determine the object- and property-specific language item to show.
- The approach used here allows plugins to easily add further validation error messages by simply using a different error type and providing the associated language item.
- - Input fields can be grouped into different `.section` elements.
- At the end of each `.section` element, there should be an template event whose name ends with `Fields`.
- The first part of the event name should reflect the type of fields in the particular `.section` element.
- Here, the input fields are just general “data” fields so that the event is called `dataFields`.
- - After the last `.section` element, fire a `section` event so that plugins can add further sections.
- - Lastly, the `.formSubmit` shows the submit button and `{csrfToken}` contains a CSRF token that is automatically validated after the form is submitted.
-
-### Person Edit Form
-
-As mentioned before, for the form to edit existing people, we only need a new controller as the template has already been implemented in a way that it handles both, adding and editing.
-
-#### `PersonEditForm`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php"
-```
-
-In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
-
-After setting a different active menu item, we declare two new properties for the edited person:
-the id of the person passed in the URL is stored in `$personID` and based on this ID, a `Person` object is created that is stored in the `$person` property.
-
-Now let use go through the different methods in chronological order again:
-
-1. `readParameters()` reads the passed ID of the edited person and creates a `Person` object based on this ID.
- If the ID is invalid, `$this->person->personID` is `null` and an `IllegalLinkException` is thrown.
-1. `readData()` only executes additional code in the case if `$_POST` is empty, thus only for the initial request before the form has been submitted.
- The data properties of `PersonAddForm` are populated with the data of the edited person so that this data is shown in the form for the initial request.
-1. `save()` handles saving the changed data.
-
- !!! warning "Do not call `parent::save()` because that would cause `PersonAddForm::save()` to be executed and thus a new person would to be created! In order for the `save` event to be fired, call `AbstractForm::save()` instead!"
-
- The only differences compared to `PersonAddForm::save()` are that we pass the edited object to the `PersonAction` constructor, execute the `update` action instead of the `create` action and do not clear the input fields after saving the changes.
-1. In `assignVariables()`, we assign the edited `Person` object to the template, which is required to create the link in the form’s action property.
- Furthermore, we assign the template variable `$action` `edit` as value.
-
- !!! info "After calling `parent::assignVariables()`, the template variable `$action` actually has the value `add` so that here, we are overwriting this already assigned value."
-
-
-## Frontend
-
-For the front end, that means the part with which the visitors of a website interact, we want to implement a simple sortable page that lists the people.
-This page should also be directly linked in the main menu.
-
-### `page.xml`
-
-First, let us register the page with the system because every front end page or form needs to be explicitly registered using the [page package installation plugin](../../package/pip/page.md):
-
-```xml
---8<-- "tutorial/tutorial-series/part-1/page.xml"
-```
-
-For more information about what each of the elements means, please refer to the [page package installation plugin page](../../package/pip/page.md).
-
-### `menuItem.xml`
-
-Next, we register the menu item using the [menuItem package installation plugin](../../package/pip/menu-item.md):
-
-```xml
---8<-- "tutorial/tutorial-series/part-1/menuItem.xml"
-```
-
-Here, the import parts are that we register the menu item for the main menu `com.woltlab.wcf.MainMenu` and link the menu item with the page `com.woltlab.wcf.people.PersonList`, which we just registered.
-
-### People List
-
-As in the ACP, we need a controller and a template.
-You might notice that both the controller’s (unqualified) class name and the template name are the same for the ACP and the front end.
-This is no problem because the qualified names of the classes differ and the files are stored in different directories and because the templates are installed by different package installation plugins and are also stored in different directories.
-
-#### `PersonListPage`
-
-```php
---8<-- "tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php"
-```
-
-This class is almost identical to the ACP version.
-In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page.
-Furthermore, `$neededPermissions` has not been set because in the front end, users do not need any special permission to access the page.
-In the front end, we explicitly set the `$defaultSortField` so that the people listed on the page are sorted by their last name (in ascending order) by default.
-
-#### `personList.tpl`
-
-```smarty
---8<-- "tutorial/tutorial-series/part-1/templates/personList.tpl"
-```
-
-If you compare this template to the one used in the ACP, you will recognize similar elements like the `.paginationTop` element, the `p.info` element if no people exist, and the `.contentFooter` element.
-Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
-
-Now, let us take a closer look at the differences:
-
-- We do not explicitly create a `.contentHeader` element but simply assign the title to the `contentTitle` variable.
- The value of the assignment is simply the title of the page and a badge showing the number of listed people.
- The `header` template that we include later will handle correctly displaying the content header on its own based on the `$contentTitle` variable.
-- Next, we create additional element for the HTML document’s `<head>` element.
- In this case, we define the [canonical link of the page](https://en.wikipedia.org/wiki/Canonical_link_element) and, because we are showing paginated content, add links to the previous and next page (if they exist).
-- We want the page to be sortable but as we will not be using a table for listing the people like in the ACP, we are not able to place links to sort the people into the table head.
- Instead, usually a box is created in the sidebar on the right-hand side that contains `select` elements to determine sort field and sort order.
-- The main part of the page is the listing of the people.
- We use a structure similar to the one used for displaying registered users.
- Here, for each person, we simply display a FontAwesome icon representing a person and show the person’s full name relying on `Person::__toString()`.
- Additionally, like in the user list, we provide the initially empty `ul.inlineList.commaSeparated` and `dl.plain.inlineDataList.small` elements that can be filled by plugins using the templates events.
-
-
-## `userGroupOption.xml`
-
-We have already used the `admin.content.canManagePeople` permissions several times, now we need to install it using the [userGroupOption package installation plugin](../../package/pip/user-group-option.md):
-
-```xml
---8<-- "tutorial/tutorial-series/part-1/userGroupOption.xml"
-```
-
-We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
-As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
-This permission is only relevant for registered users so that it should not be visible when editing the guest user group.
-This is achieved by setting `usersonly` to `1`.
-
-
-## `package.xml`
-
-Lastly, we need to create the `package.xml` file.
-For more information about this kind of file, please refer to [the `package.xml` page](../../package/package-xml.md).
-
-```xml
---8<-- "tutorial/tutorial-series/part-1/package.xml"
-```
-
-As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
-We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
-Additionally, we disallow installation of the package in the next major version `3.1` by excluding the `3.1.0 Alpha 1` version.
-This ensures that if changes from WoltLab Suite Core 3.0 to 3.1 require changing some parts of the package, it will not break the instance in which the package is installed.
-
-The most important part are to installation instructions.
-First, we install the ACP templates, files and templates, create the database table and import the language item.
-Afterwards, the ACP menu items and the permission are added.
-Now comes the part of the instructions where the order of the instructions is crucial:
-In `menuItem.xml`, we refer to the `com.woltlab.wcf.people.PersonList` page that is delivered by `page.xml`.
-As the menu item package installation plugin validates the given page and throws an exception if the page does not exist, we need to install the page before the menu item!
-
----
-
-This concludes the first part of our tutorial series after which you now have a working simple package with which you can manage people in the ACP and show the visitors of your website a simple list of all created people in the front end.
-
-The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-1).
+++ /dev/null
-# Part 2: Event Listeners and Template Listeners
-
-In the [first part](part-1.md) of this tutorial series, we have created the base structure of our people management package.
-In further parts, we will use the package of the first part as a basis to directly add new features.
-In order to explain how event listeners and template works, however, we will not directly adding a new feature to the package by altering it in this part, but we will assume that somebody else created the package and that we want to extend it the “correct” way by creating a plugin.
-
-The goal of the small plugin that will be created in this part is to add the birthday of the managed people.
-As in the first part, we will not bother with careful validation of the entered date but just make sure that it is a valid date.
-
-
-## Package Functionality
-
-The package should provide the following possibilities/functions:
-
-- List person’s birthday (if set) in people list in the ACP
-- Sort people list by birthday in the ACP
-- Add or remove birthday when adding or editing person
-- List person’s birthday (if set) in people list in the front end
-- Sort people list by birthday in the front end
-
-
-## Used Components
-
-We will use the following package installation plugins:
-
-- [acpTemplate package installation plugin](../../package/pip/acp-template.md),
-- [eventListener package installation plugin](../../package/pip/event-listener.md),
-- [file package installation plugin](../../package/pip/file.md),
-- [language package installation plugin](../../package/pip/language.md),
-- [sql package installation plugin](../../package/pip/sql.md),
-- [template package installation plugin](../../package/pip/template.md),
-- [templateListener package installation plugin](../../package/pip/template-listener.md).
-
-For more information about the event system, please refer to the [dedicated page on events](../../php/api/events.md).
-
-
-## Package Structure
-
-The package will have the following file structure:
-
-```
-├── acptemplates
-│ └── __personAddBirthday.tpl
-├── eventListener.xml
-├── files
-│ └── lib
-│ └── system
-│ └── event
-│ └── listener
-│ ├── BirthdayPersonAddFormListener.class.php
-│ └── BirthdaySortFieldPersonListPageListener.class.php
-├── install.sql
-├── language
-│ ├── de.xml
-│ └── en.xml
-├── package.xml
-├── templateListener.xml
-└── templates
- ├── __personListBirthday.tpl
- └── __personListBirthdaySortField.tpl
-```
-
-
-## Extending Person Model (`install.sql`)
-
-The existing model of a person only contains the person’s first name and their last name (in additional to the id used to identify created people).
-To add the birthday to the model, we need to create an additional database table column using the [sql package installation plugin](../../package/pip/sql.md):
-
-```sql
---8<-- "tutorial/tutorial-series/part-2/install.sql"
-```
-
-If we have a [Person object](part-1.md#person), this new property can be accessed the same way as the `personID` property, the `firstName` property, or the `lastName` property from the base package: `$person->birthday`.
-
-
-## Setting Birthday in ACP
-
-To set the birthday of a person, we need to extend the `personAdd` template to add an additional birthday field.
-This can be achieved using the `dataFields` template event at whose position we inject the following template code:
-
-```sql
---8<-- "tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl"
-```
-
-which we store in a `__personAddBirthday.tpl` template file.
-The used language item `wcf.person.birthday` is actually the only new one for this package:
-
-```sql
---8<-- "tutorial/tutorial-series/part-2/language/de.xml"
-```
-
-```sql
---8<-- "tutorial/tutorial-series/part-2/language/en.xml"
-```
-
-The template listener needs to be registered using the [templateListener package installation plugin](../../package/pip/template-listener.md).
-The corresponding complete `templateListener.xml` file is included [below](#templatelistenerxml).
-
-The template code alone is not sufficient because the `birthday` field is, at the moment, neither read, nor processed, nor saved by any PHP code.
-This can be be achieved, however, by adding event listeners to `PersonAddForm` and `PersonEditForm` which allow us to execute further code at specific location of the program.
-Before we take a look at the event listener code, we need to identify exactly which additional steps we need to undertake:
-
-1. If a person is edited and the form has not been submitted, the existing birthday of that person needs to be read.
-1. If a person is added or edited and the form has been submitted, the new birthday value needs to be read.
-1. If a person is added or edited and the form has been submitted, the new birthday value needs to be validated.
-1. If a person is added or edited and the new birthday value has been successfully validated, the new birthday value needs to be saved.
-1. If a person is added and the new birthday value has been successfully saved, the internally stored birthday needs to be reset so that the birthday field is empty when the form is shown again.
-1. The internally stored birthday value needs to be assigned to the template.
-
-The following event listeners achieves these requirements:
-
-```php
---8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php"
-```
-
-Some notes on the code:
-
-- We are inheriting from `AbstractEventListener`, instead of just implementing the `IParameterizedEventListener` interface.
- The `execute()` method of `AbstractEventListener` contains a dispatcher that automatically calls methods called `on` followed by the event name with the first character uppercased, passing the event object and the `$parameters` array.
- This simple pattern results in the event `foo` being forwarded to the method `onFoo($eventObj, $parameters)`.
-- The `birthday` column has a default value of `0000-00-00`, which we interpret as “birthday not set”.
- To show an empty input field in this case, we empty the `birthday` property after reading such a value in `readData()`.
-- The validation of the date is, as mentioned before, very basic and just checks the form of the string and uses PHP’s [checkdate](https://secure.php.net/manual/en/function.checkdate.php) function to validate the components.
-- The `save` needs to make sure that the passed date is actually a valid date and set it to `0000-00-00` if no birthday is given.
- To actually save the birthday in the database, we do not directly manipulate the database but can add an additional field to the data array passed to `PersonAction::create()` via `AbstractForm::$additionalFields`.
- As the `save` event is the last event fired before the actual save process happens, this is the perfect event to set this array element.
-
-The event listeners are installed using the `eventListener.xml` file shown [below](#eventlistenerxml).
-
-
-## Adding Birthday Table Column in ACP
-
-To add a birthday column to the person list page in the ACP, we need three parts:
-
-1. an event listener that makes the `birthday` database table column a valid sort field,
-1. a template listener that adds the birthday column to the table’s head, and
-1. a template listener that adds the birthday column to the table’s rows.
-
-The first part is a very simple class:
-
-```php
---8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php"
-```
-
-!!! info "We use `SortablePage` as a type hint instead of `wcf\acp\page\PersonListPage` because we will be using the same event listener class in the front end to also allow sorting that list by birthday."
-
-As the relevant template codes are only one line each, we will simply put them directly in the `templateListener.xml` file that will be shown [later on](#templatelistenerxml).
-The code for the table head is similar to the other `th` elements:
-
-```smarty
-<th class="columnDate columnBirthday{if $sortField == 'birthday'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=birthday&sortOrder={if $sortField == 'birthday' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.birthday{/lang}</a></th>
-```
-
-For the table body’s column, we need to make sure that the birthday is only show if it is actually set:
-
-```smarty
-<td class="columnDate columnBirthday">{if $person->birthday !== '0000-00-00'}{@$person->birthday|strtotime|date}{/if}</td>
-```
-
-
-## Adding Birthday in Front End
-
-In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person’s “statistics”.
-
-To add the birthday as a valid sort field, we use `BirthdaySortFieldPersonListPageListener` just as in the ACP.
-In the front end, we will now use a template (`__personListBirthdaySortField.tpl`) instead of a directly putting the template code in the `templateListener.xml` file:
-
-```smarty
---8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl"
-```
-
-!!! info "You might have noticed the two underscores at the beginning of the template file. For templates that are included via template listeners, this is the naming convention we use."
-
-Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable.
-
-To show the birthday, we use the following template code for the `personStatistics` template event, which again makes sure that the birthday is only shown if it is actually set:
-
-```smarty
---8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl"
-```
-
-
-## `templateListener.xml`
-
-The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
-
-```xml
---8<-- "tutorial/tutorial-series/part-2/templateListener.xml"
-```
-
-In cases where a template is used, we simply use the `include` syntax to load the template.
-
-
-## `eventListener.xml`
-
-There are two event listeners, `birthdaySortFieldAdminPersonList` and `birthdaySortFieldPersonList`, that make `birthday` a valid sort field in the ACP and the front end, respectively, and the rest takes care of setting the birthday.
-The event listener `birthdayPersonAddFormInherited` takes care of the events that are relevant for both adding and editing people, thus it listens to the `PersonAddForm` class but has `inherit` set to `1` so that it also listens to the events of the `PersonEditForm` class.
-In contrast, reading the existing birthday from a person is only relevant for editing so that the event listener `birthdayPersonEditForm` only listens to that class.
-
-```xml
---8<-- "tutorial/tutorial-series/part-2/eventListener.xml"
-```
-
-
-## `package.xml`
-
-The only relevant difference between the `package.xml` file of the base page from part 1 and the `package.xml` file of this package is that this package requires the base package `com.woltlab.wcf.people` (see `<requiredpackages>`):
-
-```xml
---8<-- "tutorial/tutorial-series/part-2/package.xml"
-```
-
----
-
-This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people.
-
-The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-2).
+++ /dev/null
-# Tutorial Series Part 3: Person Page and Comments
-
-In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details.
-To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite’s reusable comment functionality.
-
-
-## Package Functionality
-
-In addition to the existing functions from [part 1](part-1.md), the package will provide the following possibilities/functions after this part of the tutorial:
-
-- Details page for each person linked in the front end person list
-- Comment on people on their respective page (can be disabled per person)
-- User online location for person details page with name and link to person details page
-- Create menu items linking to specific person details pages
-
-
-## Used Components
-
-In addition to the components used in [part 1](part-1.md), we will use the [objectType package installation plugin](../../package/pip/object-type.md), use the [comment API](../../php/api/comments.md), create a [runtime cache](../../php/api/caches_runtime-caches.md), and create a page handler.
-
-
-## Package Structure
-
-The complete package will have the following file structure (including the files from [part 1](part-1.md)):
-
-```
-├── acpMenu.xml
-├── acptemplates
-│ ├── personAdd.tpl
-│ └── personList.tpl
-├── files
-│ └── lib
-│ ├── acp
-│ │ ├── form
-│ │ │ ├── PersonAddForm.class.php
-│ │ │ └── PersonEditForm.class.php
-│ │ └── page
-│ │ └── PersonListPage.class.php
-│ ├── data
-│ │ └── person
-│ │ ├── Person.class.php
-│ │ ├── PersonAction.class.php
-│ │ ├── PersonEditor.class.php
-│ │ └── PersonList.class.php
-│ ├── page
-│ │ ├── PersonListPage.class.php
-│ │ └── PersonPage.class.php
-│ └── system
-│ ├── cache
-│ │ └── runtime
-│ │ └── PersonRuntimeCache.class.php
-│ ├── comment
-│ │ └── manager
-│ │ └── PersonCommentManager.class.php
-│ └── page
-│ └── handler
-│ └── PersonPageHandler.class.php
-├── install.sql
-├── language
-│ ├── de.xml
-│ └── en.xml
-├── menuItem.xml
-├── objectType.xml
-├── package.xml
-├── page.xml
-├── templates
-│ ├── person.tpl
-│ └── personList.tpl
-└── userGroupOption.xml
-```
-
-!!! warning "We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new `Person::getLink()` method and new language items have been added. For all changes, please refer to the [source code on GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3)."
-
-
-## Runtime Cache
-
-To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](../../php/api/caches_runtime-caches.md) for people:
-
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php"
-```
-
-
-## Comments
-
-To allow users to comment on people, we need to tell the system that people support comments.
-This is done by registering a `com.woltlab.wcf.comment.commentableContent` object type whose processor implements [ICommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php):
-
-```xml
---8<-- "tutorial/tutorial-series/part-3/objectType.xml"
-```
-
-The `PersonCommentManager` class extended `ICommentManager`’s default implementation [AbstractCommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/AbstractCommentManager.class.php):
-
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php"
-```
-
-- First, the system is told the names of the permissions via the `$permission*` properties.
- More information about comment permissions can be found [here](../../php/api/comments.md#user-group-options).
-- The `getLink()` method returns the link to the person with the passed comment id.
- As in `isAccessible()`, `PersonRuntimeCache` is used to potentially save database queries.
-- The `isAccessible()` method checks if the active user can access the relevant person.
- As we do not have any special restrictions for accessing people, we only need to check if the person exists.
-- The `getTitle()` method returns the title used for comments and responses, which is just a generic language item in this case.
-- The `updateCounter()` updates the comments’ counter of the person.
- We have added a new `comments` database table column to the `wcf1_person` database table in order to keep track on the number of comments.
-
-Additionally, we have added a new `enableComments` database table column to the `wcf1_person` database table whose value can be set when creating or editing a person in the ACP.
-With this option, comments on individual people can be disabled.
-
-!!! info "Liking comments is already built-in and only requires some extra code in the `PersonPage` class for showing the likes of pre-loaded comments."
-
-
-## Person Page
-
-### `PersonPage`
-
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php"
-```
-
-The `PersonPage` class is similar to the `PersonEditForm` in the ACP in that it reads the id of the requested person from the request data and validates the id in `readParameters()`.
-The rest of the code only handles fetching the list of comments on the requested person.
-In `readData()`, this list is fetched using `CommentHandler::getCommentList()` if comments are enabled for the person.
-The `assignVariables()` method assigns some additional template variables like `$commentCanAdd`, which is `1` if the active person can add comments and is `0` otherwise, `$lastCommentTime`, which contains the UNIX timestamp of the last comment, and `$likeData`, which contains data related to the likes for the disabled comments.
-
-### `person.tpl`
-
-```tpl
---8<-- "tutorial/tutorial-series/part-3/templates/person.tpl"
-```
-
-For now, the `person` template is still very empty and only shows the comments in the content area.
-The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container `ul#personCommentList` element for the comments shown by `commentList` template.
-The `ul#personCommentList` elements has five additional `data-` attributes required by the JavaScript API for comments for loading more comments or creating new ones.
-The `commentListAddComment` template adds the WYSIWYG support.
-The attribute `wysiwygSelector` should be the id of the comment list `personCommentList` with an additional `AddComment` suffix.
-
-### `page.xml`
-
-```xml
---8<-- "tutorial/tutorial-series/part-3/page.xml"
-```
-
-The `page.xml` file has been extended for the new person page with identifier `com.woltlab.wcf.people.Person`.
-Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are four differences:
-
-1. It has a `<handler>` element with a class name as value.
- This aspect will be discussed in more detail in the next section.
-1. There are no `<content>` elements because, both, the title and the content of the page are dynamically generated in the template.
-1. The `<requireObjectID>` tells the system that this page requires an object id to properly work, in this case a valid person id.
-1. This page has a `<parent>` page, the person list page.
- In general, the details page for any type of object that is listed on a different page has the list page as its parent.
-
-### `PersonPageHandler`
-
-```php
---8<-- "tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php"
-```
-
-Like any page handler, the `PersonPageHandler` class has to implement the [IMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IMenuPageHandler.class.php) interface, which should be done by extending the [AbstractMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractMenuPageHandler.class.php) class.
-As we want administrators to link to specific people in menus, for example, we have to also implement the [ILookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/ILookupPageHandler.class.php) interface by extending the [AbstractLookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractLookupPageHandler.class.php) class.
-
-For the `ILookupPageHandler` interface, we need to implement three methods:
-
-1. `getLink($objectID)` returns the link to the person page with the given id.
- In this case, we simply delegate this method call to the `Person` object returned by `PersonRuntimeCache::getObject()`.
-1. `isValid($objectID)` returns `true` if the person with the given id exists, otherwise `false`.
- Here, we use `PersonRuntimeCache::getObject()` again and check if the return value is `null`, which is the case for non-existing people.
-1. `lookup($searchString)` is used when setting up an internal link and when searching for the linked person.
- This method simply searches the first and last name of the people and returns an array with the person data.
- While the `link`, the `objectID`, and the `title` element are self-explanatory, the `image` element can either contain an HTML `<img>` tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with `fa-`).
-
-Additionally, the class also implements [IOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IOnlineLocationPageHandler.class.php) which is used to determine the online location of users.
-To ensure upwards-compatibility if the `IOnlineLocationPageHandler` interface changes, the [TOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/TOnlineLocationPageHandler.class.php) trait is used.
-The `IOnlineLocationPageHandler` interface requires two methods to be implemented:
-
-1. `getOnlineLocation(Page $page, UserOnline $user)` returns the textual description of the online location.
- The language item for the user online locations should use the pattern `wcf.page.onlineLocation.{page identifier}`.
-1. `prepareOnlineLocation(Page $page, UserOnline $user)` is called for each user online before the `getOnlineLocation()` calls.
- In this case, calling `prepareOnlineLocation()` first enables us to add all relevant person ids to the person runtime cache so that for all `getOnlineLocation()` calls combined, only one database query is necessary to fetch all person objects.
-
----
-
-This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person.
-
-The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3).
-
--- /dev/null
+# Tutorial Series Part 1: Base Structure
+
+In the first part of this tutorial series, we will lay out what the basic version of package should be able to do and how to implement these functions.
+
+
+## Package Functionality
+
+The package should provide the following possibilities/functions:
+
+- Sortable list of all people in the ACP
+- Ability to add, edit and delete people in the ACP
+- Restrict the ability to add, edit and delete people (in short: manage people) in the ACP
+- Sortable list of all people in the front end
+
+
+## Used Components
+
+We will use the following package installation plugins:
+
+- [acpTemplate package installation plugin](../../package/pip/acp-template.md),
+- [acpMenu package installation plugin](../../package/pip/acp-menu.md),
+- [file package installation plugin](../../package/pip/file.md),
+- [language package installation plugin](../../package/pip/language.md),
+- [menuItem package installation plugin](../../package/pip/menu-item.md),
+- [page package installation plugin](../../package/pip/page.md),
+- [sql package installation plugin](../../package/pip/sql.md),
+- [template package installation plugin](../../package/pip/template.md),
+- [userGroupOption package installation plugin](../../package/pip/user-group-option.md),
+
+use [database objects](../../php/database-objects.md), create [pages](../../php/pages.md) and use [templates](../../view/templates.md).
+
+
+## Package Structure
+
+The package will have the following file structure:
+
+```
+├── acpMenu.xml
+├── acptemplates
+│ ├── personAdd.tpl
+│ └── personList.tpl
+├── files
+│ └── lib
+│ ├── acp
+│ │ ├── form
+│ │ │ ├── PersonAddForm.class.php
+│ │ │ └── PersonEditForm.class.php
+│ │ └── page
+│ │ └── PersonListPage.class.php
+│ ├── data
+│ │ └── person
+│ │ ├── PersonAction.class.php
+│ │ ├── Person.class.php
+│ │ ├── PersonEditor.class.php
+│ │ └── PersonList.class.php
+│ └── page
+│ └── PersonListPage.class.php
+├── install.sql
+├── language
+│ ├── de.xml
+│ └── en.xml
+├── menuItem.xml
+├── package.xml
+├── page.xml
+├── templates
+│ └── personList.tpl
+└── userGroupOption.xml
+```
+
+
+## Person Modeling
+
+### Database Table
+
+As the first step, we have to model the people we want to manage with this package.
+As this is only an introductory tutorial, we will keep things simple and only consider the first and last name of a person.
+Thus, the database table we will store the people in only contains three columns:
+
+1. `personID` is the unique numeric identifier of each person created,
+1. `firstName` contains the first name of the person,
+1. `lastName` contains the last name of the person.
+
+The first file for our package is the `install.sql` file used to create such a database table during package installation:
+
+```sql
+--8<-- "tutorial/tutorial-series/part-1/install.sql"
+```
+
+### Database Object
+
+#### `Person`
+
+In our PHP code, each person will be represented by an object of the following class:
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php"
+```
+
+The important thing here is that `Person` extends `DatabaseObject`.
+Additionally, we implement the `IRouteController` interface, which allows us to use `Person` objects to create links, and we implement PHP's magic [__toString()](https://secure.php.net/manual/en/language.oop5.magic.php#object.tostring) method for convenience.
+
+For every database object, you need to implement three additional classes:
+an action class, an editor class and a list class.
+
+#### `PersonAction`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php"
+```
+
+This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
+This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
+`$permissionsDelete` has to be set to the permission needed in order to delete a person.
+We will later use the [userGroupOption package installation plugin](../../package/pip/user-group-option.md) to create the `admin.content.canManagePeople` permission.
+`$requireACP` restricts deletion of people to the ACP.
+
+#### `PersonEditor`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php"
+```
+
+This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
+setting the static `$baseClass` property to the database object class name.
+
+#### `PersonList`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php"
+```
+
+Due to the default implementation of `DatabaseObjectList`, our `PersonList` class just needs to extend it and everything else is either automatically set by the code of `DatabaseObjectList` or, in the case of properties and methods, provided by that class.
+
+
+## ACP
+
+Next, we will take care of the controllers and views for the ACP.
+In total, we need three each:
+
+1. page to list people,
+1. form to add people, and
+1. form to edit people.
+
+Before we create the controllers and views, let us first create the menu items for the pages in the ACP menu.
+
+### ACP Menu
+
+We need to create three menu items:
+
+1. a “parent” menu item on the second level of the ACP menu item tree,
+1. a third level menu item for the people list page, and
+1. a fourth level menu item for the form to add new people.
+
+```xml
+--8<-- "tutorial/tutorial-series/part-1/acpMenu.xml"
+```
+
+We choose `wcf.acp.menu.link.content` as the parent menu item for the first menu item `wcf.acp.menu.link.person` because the people we are managing is just one form of content.
+The fourth level menu item `wcf.acp.menu.link.person.add` will only be shown as an icon and thus needs an additional element `icon` which takes a FontAwesome icon class as value.
+
+### People List
+
+To list the people in the ACP, we need a `PersonListPage` class and a `personList` template.
+
+#### `PersonListPage`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php"
+```
+
+As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
+
+1. We need to set the active ACP menu item via the `$activeMenuItem`.
+1. `$neededPermissions` contains a list of permissions of which the user needs to have at least one in order to see the person list.
+ We use the same permission for both the menu item and the page.
+1. The database object list class whose name is provided via `$objectListClassName` and that handles fetching the people from database is the `PersonList` class, which we have already created.
+1. To validate the sort field passed with the request, we set `$validSortFields` to the available database table columns.
+
+#### `personList.tpl`
+
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/acptemplates/personList.tpl"
+```
+
+We will go piece by piece through the template code:
+
+1. We include the `header` template and set the page title `wcf.acp.person.list`.
+ You have to include this template for every page!
+1. We set the content header and additional provide a button to create a new person in the content header navigation.
+1. As not all people are listed on the same page if many people have been created, we need a pagination for which we use the `pages` template plugin.
+ The `{hascontent}{content}{/content}{/hascontent}` construct ensures the `.paginationTop` element is only shown if the `pages` template plugin has a return value, thus if a pagination is necessary.
+1. Now comes the main part of the page, the list of the people, which will only be displayed if any people exist.
+ Otherwise, an info box is displayed using the generic `wcf.global.noItems` language item.
+ The `$objects` template variable is automatically assigned by `wcf\page\MultipleLinkPage` and contains the `PersonList` object used to read the people from database.
+
+ The table itself consists of a `thead` and a `tbody` element and is extendable with more columns using the template events `columnHeads` and `columns`.
+ In general, every table should provide these events.
+ The default structure of a table is used here so that the first column of the content rows contains icons to edit and to delete the row (and provides another standard event `rowButtons`) and that the second column contains the ID of the person.
+ The table can be sorted by clicking on the head of each column.
+ The used variables `$sortField` and `$sortOrder` are automatically assigned to the template by `SortablePage`.
+1. The `.contentFooter` element is only shown if people exist as it basically repeats the `.contentHeaderNavigation` and `.paginationTop` element.
+1. The JavaScript code here fulfills two duties:
+ Handling clicks on the delete icons and forwarding the requests via AJAX to the `PersonAction` class, and setting up some code that triggers if all people shown on the current page are deleted via JavaScript to either reload the page or show the `wcf.global.noItems` info box.
+1. Lastly, the `footer` template is included that terminates the page.
+ You also have to include this template for every page!
+
+Now, we have finished the page to manage the people so that we can move on to the forms with which we actually create and edit the people.
+
+### Person Add Form
+
+Like the person list, the form to add new people requires a controller class and a template.
+
+#### `PersonAddForm`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php"
+```
+
+The properties here consist of two types:
+the “housekeeping” properties `$activeMenuItem` and `$neededPermissions`, which fulfill the same roles as for `PersonListPage`, and the “data” properties `$firstName` and `$lastName`, which will contain the data entered by the user of the person to be created.
+
+Now, let's go through each method in execution order:
+
+1. `readFormParameters()` is called after the form has been submitted and reads the entered first and last name and sanitizes the values by calling `StringUtil::trim()`.
+1. `validate()` is called after the form has been submitted and is used to validate the input data.
+ In case of invalid data, the method is expected to throw a `UserInputException`.
+ Here, the validation for first and last name is the same and quite basic:
+ We check that any name has been entered and that it is not longer than the database table column permits.
+1. `save()` is called after the form has been submitted and the entered data has been validated and it creates the new person via `PersonAction`.
+ Please note that we do not just pass the first and last name to the action object but merge them with the `$this->additionalFields` array which can be used by event listeners of plugins to add additional data.
+ After creating the object, the `saved()` method is called which fires an event for plugins and the data properties are cleared so that the input fields on the page are empty so that another new person can be created.
+ Lastly, a `success` variable is assigned to the template which will show a message that the person has been successfully created.
+1. `assignVariables()` assigns the values of the “data” properties to the template and additionally assigns an `action` variable.
+ This `action` variable will be used in the template to distinguish between adding a new person and editing an existing person so that which minimal adjustments, we can use the template for both cases.
+
+#### `personAdd.tpl`
+
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl"
+```
+
+We will now only concentrate on the new parts compared to `personList.tpl`:
+
+1. We use the `$action` variable to distinguish between the languages items used for adding a person and for creating a person.
+1. Including the `formError` template automatically shows an error message if the validation failed.
+1. The `.success` element is shown after successful saving the data and, again, shows different a text depending on the executed action.
+1. The main part is the `form` element which has a common structure you will find in many forms in WoltLab Suite Core.
+ The notable parts here are:
+ - The `action` attribute of the `form` element is set depending on which controller will handle the request.
+ In the link for the edit controller, we can now simply pass the edited `Person` object directly as the `Person` class implements the `IRouteController` interface.
+ - The field that caused the validation error can be accessed via `$errorField`.
+ - The type of the validation error can be accessed via `$errorType`.
+ For an empty input field, we show the generic `wcf.global.form.error.empty` language item.
+ In all other cases, we use the error type to determine the object- and property-specific language item to show.
+ The approach used here allows plugins to easily add further validation error messages by simply using a different error type and providing the associated language item.
+ - Input fields can be grouped into different `.section` elements.
+ At the end of each `.section` element, there should be an template event whose name ends with `Fields`.
+ The first part of the event name should reflect the type of fields in the particular `.section` element.
+ Here, the input fields are just general “data” fields so that the event is called `dataFields`.
+ - After the last `.section` element, fire a `section` event so that plugins can add further sections.
+ - Lastly, the `.formSubmit` shows the submit button and `{csrfToken}` contains a CSRF token that is automatically validated after the form is submitted.
+
+### Person Edit Form
+
+As mentioned before, for the form to edit existing people, we only need a new controller as the template has already been implemented in a way that it handles both, adding and editing.
+
+#### `PersonEditForm`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php"
+```
+
+In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
+
+After setting a different active menu item, we declare two new properties for the edited person:
+the id of the person passed in the URL is stored in `$personID` and based on this ID, a `Person` object is created that is stored in the `$person` property.
+
+Now let use go through the different methods in chronological order again:
+
+1. `readParameters()` reads the passed ID of the edited person and creates a `Person` object based on this ID.
+ If the ID is invalid, `$this->person->personID` is `null` and an `IllegalLinkException` is thrown.
+1. `readData()` only executes additional code in the case if `$_POST` is empty, thus only for the initial request before the form has been submitted.
+ The data properties of `PersonAddForm` are populated with the data of the edited person so that this data is shown in the form for the initial request.
+1. `save()` handles saving the changed data.
+
+ !!! warning "Do not call `parent::save()` because that would cause `PersonAddForm::save()` to be executed and thus a new person would to be created! In order for the `save` event to be fired, call `AbstractForm::save()` instead!"
+
+ The only differences compared to `PersonAddForm::save()` are that we pass the edited object to the `PersonAction` constructor, execute the `update` action instead of the `create` action and do not clear the input fields after saving the changes.
+1. In `assignVariables()`, we assign the edited `Person` object to the template, which is required to create the link in the form’s action property.
+ Furthermore, we assign the template variable `$action` `edit` as value.
+
+ !!! info "After calling `parent::assignVariables()`, the template variable `$action` actually has the value `add` so that here, we are overwriting this already assigned value."
+
+
+## Frontend
+
+For the front end, that means the part with which the visitors of a website interact, we want to implement a simple sortable page that lists the people.
+This page should also be directly linked in the main menu.
+
+### `page.xml`
+
+First, let us register the page with the system because every front end page or form needs to be explicitly registered using the [page package installation plugin](../../package/pip/page.md):
+
+```xml
+--8<-- "tutorial/tutorial-series/part-1/page.xml"
+```
+
+For more information about what each of the elements means, please refer to the [page package installation plugin page](../../package/pip/page.md).
+
+### `menuItem.xml`
+
+Next, we register the menu item using the [menuItem package installation plugin](../../package/pip/menu-item.md):
+
+```xml
+--8<-- "tutorial/tutorial-series/part-1/menuItem.xml"
+```
+
+Here, the import parts are that we register the menu item for the main menu `com.woltlab.wcf.MainMenu` and link the menu item with the page `com.woltlab.wcf.people.PersonList`, which we just registered.
+
+### People List
+
+As in the ACP, we need a controller and a template.
+You might notice that both the controller’s (unqualified) class name and the template name are the same for the ACP and the front end.
+This is no problem because the qualified names of the classes differ and the files are stored in different directories and because the templates are installed by different package installation plugins and are also stored in different directories.
+
+#### `PersonListPage`
+
+```php
+--8<-- "tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php"
+```
+
+This class is almost identical to the ACP version.
+In the front end, we do not need to set the active menu item manually because the system determines the active menu item automatically based on the requested page.
+Furthermore, `$neededPermissions` has not been set because in the front end, users do not need any special permission to access the page.
+In the front end, we explicitly set the `$defaultSortField` so that the people listed on the page are sorted by their last name (in ascending order) by default.
+
+#### `personList.tpl`
+
+```smarty
+--8<-- "tutorial/tutorial-series/part-1/templates/personList.tpl"
+```
+
+If you compare this template to the one used in the ACP, you will recognize similar elements like the `.paginationTop` element, the `p.info` element if no people exist, and the `.contentFooter` element.
+Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
+
+Now, let us take a closer look at the differences:
+
+- We do not explicitly create a `.contentHeader` element but simply assign the title to the `contentTitle` variable.
+ The value of the assignment is simply the title of the page and a badge showing the number of listed people.
+ The `header` template that we include later will handle correctly displaying the content header on its own based on the `$contentTitle` variable.
+- Next, we create additional element for the HTML document’s `<head>` element.
+ In this case, we define the [canonical link of the page](https://en.wikipedia.org/wiki/Canonical_link_element) and, because we are showing paginated content, add links to the previous and next page (if they exist).
+- We want the page to be sortable but as we will not be using a table for listing the people like in the ACP, we are not able to place links to sort the people into the table head.
+ Instead, usually a box is created in the sidebar on the right-hand side that contains `select` elements to determine sort field and sort order.
+- The main part of the page is the listing of the people.
+ We use a structure similar to the one used for displaying registered users.
+ Here, for each person, we simply display a FontAwesome icon representing a person and show the person’s full name relying on `Person::__toString()`.
+ Additionally, like in the user list, we provide the initially empty `ul.inlineList.commaSeparated` and `dl.plain.inlineDataList.small` elements that can be filled by plugins using the templates events.
+
+
+## `userGroupOption.xml`
+
+We have already used the `admin.content.canManagePeople` permissions several times, now we need to install it using the [userGroupOption package installation plugin](../../package/pip/user-group-option.md):
+
+```xml
+--8<-- "tutorial/tutorial-series/part-1/userGroupOption.xml"
+```
+
+We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
+As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
+This permission is only relevant for registered users so that it should not be visible when editing the guest user group.
+This is achieved by setting `usersonly` to `1`.
+
+
+## `package.xml`
+
+Lastly, we need to create the `package.xml` file.
+For more information about this kind of file, please refer to [the `package.xml` page](../../package/package-xml.md).
+
+```xml
+--8<-- "tutorial/tutorial-series/part-1/package.xml"
+```
+
+As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
+We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
+Additionally, we disallow installation of the package in the next major version `3.1` by excluding the `3.1.0 Alpha 1` version.
+This ensures that if changes from WoltLab Suite Core 3.0 to 3.1 require changing some parts of the package, it will not break the instance in which the package is installed.
+
+The most important part are to installation instructions.
+First, we install the ACP templates, files and templates, create the database table and import the language item.
+Afterwards, the ACP menu items and the permission are added.
+Now comes the part of the instructions where the order of the instructions is crucial:
+In `menuItem.xml`, we refer to the `com.woltlab.wcf.people.PersonList` page that is delivered by `page.xml`.
+As the menu item package installation plugin validates the given page and throws an exception if the page does not exist, we need to install the page before the menu item!
+
+---
+
+This concludes the first part of our tutorial series after which you now have a working simple package with which you can manage people in the ACP and show the visitors of your website a simple list of all created people in the front end.
+
+The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-1).
--- /dev/null
+# Part 2: Event Listeners and Template Listeners
+
+In the [first part](part_1.md) of this tutorial series, we have created the base structure of our people management package.
+In further parts, we will use the package of the first part as a basis to directly add new features.
+In order to explain how event listeners and template works, however, we will not directly adding a new feature to the package by altering it in this part, but we will assume that somebody else created the package and that we want to extend it the “correct” way by creating a plugin.
+
+The goal of the small plugin that will be created in this part is to add the birthday of the managed people.
+As in the first part, we will not bother with careful validation of the entered date but just make sure that it is a valid date.
+
+
+## Package Functionality
+
+The package should provide the following possibilities/functions:
+
+- List person’s birthday (if set) in people list in the ACP
+- Sort people list by birthday in the ACP
+- Add or remove birthday when adding or editing person
+- List person’s birthday (if set) in people list in the front end
+- Sort people list by birthday in the front end
+
+
+## Used Components
+
+We will use the following package installation plugins:
+
+- [acpTemplate package installation plugin](../../package/pip/acp-template.md),
+- [eventListener package installation plugin](../../package/pip/event-listener.md),
+- [file package installation plugin](../../package/pip/file.md),
+- [language package installation plugin](../../package/pip/language.md),
+- [sql package installation plugin](../../package/pip/sql.md),
+- [template package installation plugin](../../package/pip/template.md),
+- [templateListener package installation plugin](../../package/pip/template-listener.md).
+
+For more information about the event system, please refer to the [dedicated page on events](../../php/api/events.md).
+
+
+## Package Structure
+
+The package will have the following file structure:
+
+```
+├── acptemplates
+│ └── __personAddBirthday.tpl
+├── eventListener.xml
+├── files
+│ └── lib
+│ └── system
+│ └── event
+│ └── listener
+│ ├── BirthdayPersonAddFormListener.class.php
+│ └── BirthdaySortFieldPersonListPageListener.class.php
+├── install.sql
+├── language
+│ ├── de.xml
+│ └── en.xml
+├── package.xml
+├── templateListener.xml
+└── templates
+ ├── __personListBirthday.tpl
+ └── __personListBirthdaySortField.tpl
+```
+
+
+## Extending Person Model (`install.sql`)
+
+The existing model of a person only contains the person’s first name and their last name (in additional to the id used to identify created people).
+To add the birthday to the model, we need to create an additional database table column using the [sql package installation plugin](../../package/pip/sql.md):
+
+```sql
+--8<-- "tutorial/tutorial-series/part-2/install.sql"
+```
+
+If we have a [Person object](part_1.md#person), this new property can be accessed the same way as the `personID` property, the `firstName` property, or the `lastName` property from the base package: `$person->birthday`.
+
+
+## Setting Birthday in ACP
+
+To set the birthday of a person, we need to extend the `personAdd` template to add an additional birthday field.
+This can be achieved using the `dataFields` template event at whose position we inject the following template code:
+
+```sql
+--8<-- "tutorial/tutorial-series/part-2/acptemplates/__personAddBirthday.tpl"
+```
+
+which we store in a `__personAddBirthday.tpl` template file.
+The used language item `wcf.person.birthday` is actually the only new one for this package:
+
+```sql
+--8<-- "tutorial/tutorial-series/part-2/language/de.xml"
+```
+
+```sql
+--8<-- "tutorial/tutorial-series/part-2/language/en.xml"
+```
+
+The template listener needs to be registered using the [templateListener package installation plugin](../../package/pip/template-listener.md).
+The corresponding complete `templateListener.xml` file is included [below](#templatelistenerxml).
+
+The template code alone is not sufficient because the `birthday` field is, at the moment, neither read, nor processed, nor saved by any PHP code.
+This can be be achieved, however, by adding event listeners to `PersonAddForm` and `PersonEditForm` which allow us to execute further code at specific location of the program.
+Before we take a look at the event listener code, we need to identify exactly which additional steps we need to undertake:
+
+1. If a person is edited and the form has not been submitted, the existing birthday of that person needs to be read.
+1. If a person is added or edited and the form has been submitted, the new birthday value needs to be read.
+1. If a person is added or edited and the form has been submitted, the new birthday value needs to be validated.
+1. If a person is added or edited and the new birthday value has been successfully validated, the new birthday value needs to be saved.
+1. If a person is added and the new birthday value has been successfully saved, the internally stored birthday needs to be reset so that the birthday field is empty when the form is shown again.
+1. The internally stored birthday value needs to be assigned to the template.
+
+The following event listeners achieves these requirements:
+
+```php
+--8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdayPersonAddFormListener.class.php"
+```
+
+Some notes on the code:
+
+- We are inheriting from `AbstractEventListener`, instead of just implementing the `IParameterizedEventListener` interface.
+ The `execute()` method of `AbstractEventListener` contains a dispatcher that automatically calls methods called `on` followed by the event name with the first character uppercased, passing the event object and the `$parameters` array.
+ This simple pattern results in the event `foo` being forwarded to the method `onFoo($eventObj, $parameters)`.
+- The `birthday` column has a default value of `0000-00-00`, which we interpret as “birthday not set”.
+ To show an empty input field in this case, we empty the `birthday` property after reading such a value in `readData()`.
+- The validation of the date is, as mentioned before, very basic and just checks the form of the string and uses PHP’s [checkdate](https://secure.php.net/manual/en/function.checkdate.php) function to validate the components.
+- The `save` needs to make sure that the passed date is actually a valid date and set it to `0000-00-00` if no birthday is given.
+ To actually save the birthday in the database, we do not directly manipulate the database but can add an additional field to the data array passed to `PersonAction::create()` via `AbstractForm::$additionalFields`.
+ As the `save` event is the last event fired before the actual save process happens, this is the perfect event to set this array element.
+
+The event listeners are installed using the `eventListener.xml` file shown [below](#eventlistenerxml).
+
+
+## Adding Birthday Table Column in ACP
+
+To add a birthday column to the person list page in the ACP, we need three parts:
+
+1. an event listener that makes the `birthday` database table column a valid sort field,
+1. a template listener that adds the birthday column to the table’s head, and
+1. a template listener that adds the birthday column to the table’s rows.
+
+The first part is a very simple class:
+
+```php
+--8<-- "tutorial/tutorial-series/part-2/files/lib/system/event/listener/BirthdaySortFieldPersonListPageListener.class.php"
+```
+
+!!! info "We use `SortablePage` as a type hint instead of `wcf\acp\page\PersonListPage` because we will be using the same event listener class in the front end to also allow sorting that list by birthday."
+
+As the relevant template codes are only one line each, we will simply put them directly in the `templateListener.xml` file that will be shown [later on](#templatelistenerxml).
+The code for the table head is similar to the other `th` elements:
+
+```smarty
+<th class="columnDate columnBirthday{if $sortField == 'birthday'} active {@$sortOrder}{/if}"><a href="{link controller='PersonList'}pageNo={@$pageNo}&sortField=birthday&sortOrder={if $sortField == 'birthday' && $sortOrder == 'ASC'}DESC{else}ASC{/if}{/link}">{lang}wcf.person.birthday{/lang}</a></th>
+```
+
+For the table body’s column, we need to make sure that the birthday is only show if it is actually set:
+
+```smarty
+<td class="columnDate columnBirthday">{if $person->birthday !== '0000-00-00'}{@$person->birthday|strtotime|date}{/if}</td>
+```
+
+
+## Adding Birthday in Front End
+
+In the front end, we also want to make the list sortable by birthday and show the birthday as part of each person’s “statistics”.
+
+To add the birthday as a valid sort field, we use `BirthdaySortFieldPersonListPageListener` just as in the ACP.
+In the front end, we will now use a template (`__personListBirthdaySortField.tpl`) instead of a directly putting the template code in the `templateListener.xml` file:
+
+```smarty
+--8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthdaySortField.tpl"
+```
+
+!!! info "You might have noticed the two underscores at the beginning of the template file. For templates that are included via template listeners, this is the naming convention we use."
+
+Putting the template code into a file has the advantage that in the administrator is able to edit the code directly via a custom template group, even though in this case this might not be very probable.
+
+To show the birthday, we use the following template code for the `personStatistics` template event, which again makes sure that the birthday is only shown if it is actually set:
+
+```smarty
+--8<-- "tutorial/tutorial-series/part-2/templates/__personListBirthday.tpl"
+```
+
+
+## `templateListener.xml`
+
+The following code shows the `templateListener.xml` file used to install all mentioned template listeners:
+
+```xml
+--8<-- "tutorial/tutorial-series/part-2/templateListener.xml"
+```
+
+In cases where a template is used, we simply use the `include` syntax to load the template.
+
+
+## `eventListener.xml`
+
+There are two event listeners, `birthdaySortFieldAdminPersonList` and `birthdaySortFieldPersonList`, that make `birthday` a valid sort field in the ACP and the front end, respectively, and the rest takes care of setting the birthday.
+The event listener `birthdayPersonAddFormInherited` takes care of the events that are relevant for both adding and editing people, thus it listens to the `PersonAddForm` class but has `inherit` set to `1` so that it also listens to the events of the `PersonEditForm` class.
+In contrast, reading the existing birthday from a person is only relevant for editing so that the event listener `birthdayPersonEditForm` only listens to that class.
+
+```xml
+--8<-- "tutorial/tutorial-series/part-2/eventListener.xml"
+```
+
+
+## `package.xml`
+
+The only relevant difference between the `package.xml` file of the base page from part 1 and the `package.xml` file of this package is that this package requires the base package `com.woltlab.wcf.people` (see `<requiredpackages>`):
+
+```xml
+--8<-- "tutorial/tutorial-series/part-2/package.xml"
+```
+
+---
+
+This concludes the second part of our tutorial series after which you now have extended the base package using event listeners and template listeners that allow you to enter the birthday of the people.
+
+The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-2).
--- /dev/null
+# Tutorial Series Part 3: Person Page and Comments
+
+In this part of our tutorial series, we will add a new front end page to our package that is dedicated to each person and shows their personal details.
+To make good use of this new page and introduce a new API of WoltLab Suite, we will add the opportunity for users to comment on the person using WoltLab Suite’s reusable comment functionality.
+
+
+## Package Functionality
+
+In addition to the existing functions from [part 1](part_1.md), the package will provide the following possibilities/functions after this part of the tutorial:
+
+- Details page for each person linked in the front end person list
+- Comment on people on their respective page (can be disabled per person)
+- User online location for person details page with name and link to person details page
+- Create menu items linking to specific person details pages
+
+
+## Used Components
+
+In addition to the components used in [part 1](part_1.md), we will use the [objectType package installation plugin](../../package/pip/object-type.md), use the [comment API](../../php/api/comments.md), create a [runtime cache](../../php/api/caches_runtime-caches.md), and create a page handler.
+
+
+## Package Structure
+
+The complete package will have the following file structure (including the files from [part 1](part_1.md)):
+
+```
+├── acpMenu.xml
+├── acptemplates
+│ ├── personAdd.tpl
+│ └── personList.tpl
+├── files
+│ └── lib
+│ ├── acp
+│ │ ├── form
+│ │ │ ├── PersonAddForm.class.php
+│ │ │ └── PersonEditForm.class.php
+│ │ └── page
+│ │ └── PersonListPage.class.php
+│ ├── data
+│ │ └── person
+│ │ ├── Person.class.php
+│ │ ├── PersonAction.class.php
+│ │ ├── PersonEditor.class.php
+│ │ └── PersonList.class.php
+│ ├── page
+│ │ ├── PersonListPage.class.php
+│ │ └── PersonPage.class.php
+│ └── system
+│ ├── cache
+│ │ └── runtime
+│ │ └── PersonRuntimeCache.class.php
+│ ├── comment
+│ │ └── manager
+│ │ └── PersonCommentManager.class.php
+│ └── page
+│ └── handler
+│ └── PersonPageHandler.class.php
+├── install.sql
+├── language
+│ ├── de.xml
+│ └── en.xml
+├── menuItem.xml
+├── objectType.xml
+├── package.xml
+├── page.xml
+├── templates
+│ ├── person.tpl
+│ └── personList.tpl
+└── userGroupOption.xml
+```
+
+!!! warning "We will not mention every code change between the first part and this part, as we only want to focus on the important, new parts of the code. For example, there is a new `Person::getLink()` method and new language items have been added. For all changes, please refer to the [source code on GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3)."
+
+
+## Runtime Cache
+
+To reduce the number of database queries when different APIs require person objects, we implement a [runtime cache](../../php/api/caches_runtime-caches.md) for people:
+
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/cache/runtime/PersonRuntimeCache.class.php"
+```
+
+
+## Comments
+
+To allow users to comment on people, we need to tell the system that people support comments.
+This is done by registering a `com.woltlab.wcf.comment.commentableContent` object type whose processor implements [ICommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/ICommentManager.class.php):
+
+```xml
+--8<-- "tutorial/tutorial-series/part-3/objectType.xml"
+```
+
+The `PersonCommentManager` class extended `ICommentManager`’s default implementation [AbstractCommentManager](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/comment/manager/AbstractCommentManager.class.php):
+
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/comment/manager/PersonCommentManager.class.php"
+```
+
+- First, the system is told the names of the permissions via the `$permission*` properties.
+ More information about comment permissions can be found [here](../../php/api/comments.md#user-group-options).
+- The `getLink()` method returns the link to the person with the passed comment id.
+ As in `isAccessible()`, `PersonRuntimeCache` is used to potentially save database queries.
+- The `isAccessible()` method checks if the active user can access the relevant person.
+ As we do not have any special restrictions for accessing people, we only need to check if the person exists.
+- The `getTitle()` method returns the title used for comments and responses, which is just a generic language item in this case.
+- The `updateCounter()` updates the comments’ counter of the person.
+ We have added a new `comments` database table column to the `wcf1_person` database table in order to keep track on the number of comments.
+
+Additionally, we have added a new `enableComments` database table column to the `wcf1_person` database table whose value can be set when creating or editing a person in the ACP.
+With this option, comments on individual people can be disabled.
+
+!!! info "Liking comments is already built-in and only requires some extra code in the `PersonPage` class for showing the likes of pre-loaded comments."
+
+
+## Person Page
+
+### `PersonPage`
+
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/page/PersonPage.class.php"
+```
+
+The `PersonPage` class is similar to the `PersonEditForm` in the ACP in that it reads the id of the requested person from the request data and validates the id in `readParameters()`.
+The rest of the code only handles fetching the list of comments on the requested person.
+In `readData()`, this list is fetched using `CommentHandler::getCommentList()` if comments are enabled for the person.
+The `assignVariables()` method assigns some additional template variables like `$commentCanAdd`, which is `1` if the active person can add comments and is `0` otherwise, `$lastCommentTime`, which contains the UNIX timestamp of the last comment, and `$likeData`, which contains data related to the likes for the disabled comments.
+
+### `person.tpl`
+
+```tpl
+--8<-- "tutorial/tutorial-series/part-3/templates/person.tpl"
+```
+
+For now, the `person` template is still very empty and only shows the comments in the content area.
+The template code shown for comments is very generic and used in this form in many locations as it only sets the header of the comment list and the container `ul#personCommentList` element for the comments shown by `commentList` template.
+The `ul#personCommentList` elements has five additional `data-` attributes required by the JavaScript API for comments for loading more comments or creating new ones.
+The `commentListAddComment` template adds the WYSIWYG support.
+The attribute `wysiwygSelector` should be the id of the comment list `personCommentList` with an additional `AddComment` suffix.
+
+### `page.xml`
+
+```xml
+--8<-- "tutorial/tutorial-series/part-3/page.xml"
+```
+
+The `page.xml` file has been extended for the new person page with identifier `com.woltlab.wcf.people.Person`.
+Compared to the pre-existing `com.woltlab.wcf.people.PersonList` page, there are four differences:
+
+1. It has a `<handler>` element with a class name as value.
+ This aspect will be discussed in more detail in the next section.
+1. There are no `<content>` elements because, both, the title and the content of the page are dynamically generated in the template.
+1. The `<requireObjectID>` tells the system that this page requires an object id to properly work, in this case a valid person id.
+1. This page has a `<parent>` page, the person list page.
+ In general, the details page for any type of object that is listed on a different page has the list page as its parent.
+
+### `PersonPageHandler`
+
+```php
+--8<-- "tutorial/tutorial-series/part-3/files/lib/system/page/handler/PersonPageHandler.class.php"
+```
+
+Like any page handler, the `PersonPageHandler` class has to implement the [IMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IMenuPageHandler.class.php) interface, which should be done by extending the [AbstractMenuPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractMenuPageHandler.class.php) class.
+As we want administrators to link to specific people in menus, for example, we have to also implement the [ILookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/ILookupPageHandler.class.php) interface by extending the [AbstractLookupPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/AbstractLookupPageHandler.class.php) class.
+
+For the `ILookupPageHandler` interface, we need to implement three methods:
+
+1. `getLink($objectID)` returns the link to the person page with the given id.
+ In this case, we simply delegate this method call to the `Person` object returned by `PersonRuntimeCache::getObject()`.
+1. `isValid($objectID)` returns `true` if the person with the given id exists, otherwise `false`.
+ Here, we use `PersonRuntimeCache::getObject()` again and check if the return value is `null`, which is the case for non-existing people.
+1. `lookup($searchString)` is used when setting up an internal link and when searching for the linked person.
+ This method simply searches the first and last name of the people and returns an array with the person data.
+ While the `link`, the `objectID`, and the `title` element are self-explanatory, the `image` element can either contain an HTML `<img>` tag, which is displayed next to the search result (WoltLab Suite uses an image tag for users showing their avatar, for example), or a FontAwesome icon class (starting with `fa-`).
+
+Additionally, the class also implements [IOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/IOnlineLocationPageHandler.class.php) which is used to determine the online location of users.
+To ensure upwards-compatibility if the `IOnlineLocationPageHandler` interface changes, the [TOnlineLocationPageHandler](https://github.com/WoltLab/WCF/blob/master/wcfsetup/install/files/lib/system/page/handler/TOnlineLocationPageHandler.class.php) trait is used.
+The `IOnlineLocationPageHandler` interface requires two methods to be implemented:
+
+1. `getOnlineLocation(Page $page, UserOnline $user)` returns the textual description of the online location.
+ The language item for the user online locations should use the pattern `wcf.page.onlineLocation.{page identifier}`.
+1. `prepareOnlineLocation(Page $page, UserOnline $user)` is called for each user online before the `getOnlineLocation()` calls.
+ In this case, calling `prepareOnlineLocation()` first enables us to add all relevant person ids to the person runtime cache so that for all `getOnlineLocation()` calls combined, only one database query is necessary to fetch all person objects.
+
+---
+
+This concludes the third part of our tutorial series after which each person has a dedicated page on which people can comment on the person.
+
+The complete source code of this part can be found on [GitHub](https://github.com/WoltLab/woltlab.github.io/tree/master/_includes/tutorial/tutorial-series/part-3).
+
- 'Package Components': 'migration/wcf21/package.md'
- 'Tutorials':
- - 'Tutorial Series': 'tutorial/series/overview.md'
+ - 'Tutorial Series':
+ - 'Overview': 'tutorial/series/overview.md'
+ - 'Part 1': 'tutorial/series/part_1.md'
+ - 'Part 2': 'tutorial/series/part_2.md'
+ - 'Part 3': 'tutorial/series/part_3.md'
markdown_extensions:
- toc: