f86530539bd6fa3e61756f4d22bfaa59cff88a6a
[GitHub/WoltLab/woltlab.github.io.git] / docs / tutorial_tutorial-series_part-1-base-structure.md
1 ---
2 title: "Tutorial Series Part 1: Base Structure"
3 sidebar: sidebar
4 permalink: tutorial_tutorial-series_part-1-base-structure.html
5 folder: tutorial/tutorial-series
6 parent: tutorial_tutorial-series
7 ---
8
9 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.
10
11
12 ## Package Functionality
13
14 The package should provide the following possibilities/functions:
15
16 - Sortable list of all people in the ACP
17 - Ability to add, edit and delete people in the ACP
18 - Restrict the ability to add, edit and delete people (in short: manage people) in the ACP
19 - Sortable list of all people in the front end
20
21
22 ## Used Components
23
24 We will use the following package installation plugins:
25
26 - [acpTemplate package installation plugin](package_pip_acp-template.html),
27 - [acpMenu package installation plugin](package_pip_acp-menu.html),
28 - [file package installation plugin](package_pip_file.html),
29 - [language package installation plugin](package_pip_language.html),
30 - [menuItem package installation plugin](package_pip_menu-item.html),
31 - [page package installation plugin](package_pip_page.html),
32 - [sql package installation plugin](package_pip_sql.html),
33 - [template package installation plugin](package_pip_template.html),
34 - [userGroupOption package installation plugin](package_pip_user-group-option.html),
35
36 use [database objects](php_database-objects.html), create [pages](php_pages.html) and use [templates](view_templates.html).
37
38
39 ## Package Structure
40
41 The package will have the following file structure:
42
43 ```
44 ├── acpMenu.xml
45 ├── acptemplates
46 │   ├── personAdd.tpl
47 │   └── personList.tpl
48 ├── files
49 │   └── lib
50 │   ├── acp
51 │   │   ├── form
52 │   │   │   ├── PersonAddForm.class.php
53 │   │   │   └── PersonEditForm.class.php
54 │   │   └── page
55 │   │   └── PersonListPage.class.php
56 │   ├── data
57 │   │   └── person
58 │   │   ├── PersonAction.class.php
59 │   │   ├── Person.class.php
60 │   │   ├── PersonEditor.class.php
61 │   │   └── PersonList.class.php
62 │   └── page
63 │   └── PersonListPage.class.php
64 ├── install.sql
65 ├── language
66 │   ├── de.xml
67 │   └── en.xml
68 ├── menuItem.xml
69 ├── package.xml
70 ├── page.xml
71 ├── templates
72 │   └── personList.tpl
73 └── userGroupOption.xml
74 ```
75
76
77 ## Person Modeling
78
79 ### Database Table
80
81 As the first step, we have to model the people we want to manage with this package.
82 As this is only an introductory tutorial, we will keep things simple and only consider the first and last name of a person.
83 Thus, the database table we will store the people in only contains three columns:
84
85 1. `personID` is the unique numeric identifier of each person created,
86 1. `firstName` contains the first name of the person,
87 1. `lastName` contains the last name of the person.
88
89 The first file for our package is the `install.sql` file used to create such a database table during package installation:
90
91 {% highlight sql %}
92 {% include tutorial/tutorial-series/part-1/install.sql %}
93 {% endhighlight %}
94
95 ### Database Object
96
97 #### `Person`
98
99 In our PHP code, each person will be represented by an object of the following class:
100
101 {% highlight php %}
102 {% include tutorial/tutorial-series/part-1/files/lib/data/person/Person.class.php %}
103 {% endhighlight %}
104
105 The important thing here is that `Person` extends `DatabaseObject`.
106 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.
107
108 For every database object, you need to implement three additional classes:
109 an action class, an editor class and a list class.
110
111 #### `PersonAction`
112
113 {% highlight php %}
114 {% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonAction.class.php %}
115 {% endhighlight %}
116
117 This implementation of `AbstractDatabaseObjectAction` is very basic and only sets the `$permissionsDelete` and `$requireACP` properties.
118 This is done so that later on, when implementing the people list for the ACP, we can delete people simply via AJAX.
119 `$permissionsDelete` has to be set to the permission needed in order to delete a person.
120 We will later use the [userGroupOption package installation plugin](package_pip_user-group-option.html) to create the `admin.content.canManagePeople` permission.
121 `$requireACP` restricts deletion of people to the ACP.
122
123 #### `PersonEditor`
124
125 {% highlight php %}
126 {% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonEditor.class.php %}
127 {% endhighlight %}
128
129 This implementation of `DatabaseObjectEditor` fulfills the minimum requirement for a database object editor:
130 setting the static `$baseClass` property to the database object class name.
131
132 #### `PersonList`
133
134 {% highlight php %}
135 {% include tutorial/tutorial-series/part-1/files/lib/data/person/PersonList.class.php %}
136 {% endhighlight %}
137
138 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.
139
140
141 ## ACP
142
143 Next, we will take care of the controllers and views for the ACP.
144 In total, we need three each:
145
146 1. page to list people,
147 1. form to add people, and
148 1. form to edit people.
149
150 Before we create the controllers and views, let us first create the menu items for the pages in the ACP menu.
151
152 ### ACP Menu
153
154 We need to create three menu items:
155
156 1. a “parent” menu item on the second level of the ACP menu item tree,
157 1. a third level menu item for the people list page, and
158 1. a fourth level menu item for the form to add new people.
159
160 {% highlight xml %}
161 {% include tutorial/tutorial-series/part-1/acpMenu.xml %}
162 {% endhighlight %}
163
164 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.
165 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.
166
167 ### People List
168
169 To list the people in the ACP, we need a `PersonListPage` class and a `personList` template.
170
171 #### `PersonListPage`
172
173 {% highlight php %}
174 {% include tutorial/tutorial-series/part-1/files/lib/acp/page/PersonListPage.class.php %}
175 {% endhighlight %}
176
177 As WoltLab Suite Core already provides a powerful default implementation of a sortable page, our work here is minimal:
178
179 1. We need to set the active ACP menu item via the `$activeMenuItem`.
180 1. `$neededPermissions` contains a list of permissions of which the user needs to have at least one in order to see the person list.
181 We use the same permission for both the menu item and the page.
182 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.
183 1. To validate the sort field passed with the request, we set `$validSortFields` to the available database table columns.
184
185 #### `personList.tpl`
186
187 {% highlight smarty %}
188 {% include tutorial/tutorial-series/part-1/acptemplates/personList.tpl %}
189 {% endhighlight %}
190
191 We will go piece by piece through the template code:
192
193 1. We include the `header` template and set the page title `wcf.acp.person.list`.
194 You have to include this template for every page!
195 1. We set the content header and additional provide a button to create a new person in the content header navigation.
196 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.
197 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.
198 1. Now comes the main part of the page, the list of the people, which will only be displayed if any people exist.
199 Otherwise, an info box is displayed using the generic `wcf.global.noItems` language item.
200 The `$objects` template variable is automatically assigned by `wcf\page\MultipleLinkPage` and contains the `PersonList` object used to read the people from database.
201
202 The table itself consists of a `thead` and a `tbody` element and is extendable with more columns using the template events `columnHeads` and `columns`.
203 In general, every table should provide these events.
204 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.
205 The table can be sorted by clicking on the head of each column.
206 The used variables `$sortField` and `$sortOrder` are automatically assigned to the template by `SortablePage`.
207 1. The `.contentFooter` element is only shown if people exist as it basically repeats the `.contentHeaderNavigation` and `.paginationTop` element.
208 1. The JavaScript code here fulfills two duties:
209 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.
210 1. Lastly, the `footer` template is included that terminates the page.
211 You also have to include this template for every page!
212
213 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.
214
215 ### Person Add Form
216
217 Like the person list, the form to add new people requires a controller class and a template.
218
219 #### `PersonAddForm`
220
221 {% highlight php %}
222 {% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonAddForm.class.php %}
223 {% endhighlight %}
224
225 The properties here consist of two types:
226 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.
227
228 Now, let's go through each method in execution order:
229
230 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()`.
231 1. `validate()` is called after the form has been submitted and is used to validate the input data.
232 In case of invalid data, the method is expected to throw a `UserInputException`.
233 Here, the validation for first and last name is the same and quite basic:
234 We check that any name has been entered and that it is not longer than the database table column permits.
235 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`.
236 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.
237 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.
238 Lastly, a `success` variable is assigned to the template which will show a message that the person has been successfully created.
239 1. `assignVariables()` assigns the values of the “data” properties to the template and additionally assigns an `action` variable.
240 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.
241
242 #### `personAdd.tpl`
243
244 {% highlight smarty %}
245 {% include tutorial/tutorial-series/part-1/acptemplates/personAdd.tpl %}
246 {% endhighlight %}
247
248 We will now only concentrate on the new parts compared to `personList.tpl`:
249
250 1. We use the `$action` variable to distinguish between the languages items used for adding a person and for creating a person.
251 1. Including the `formError` template automatically shows an error message if the validation failed.
252 1. The `.success` element is shown after successful saving the data and, again, shows different a text depending on the executed action.
253 1. The main part is the `form` element which has a common structure you will find in many forms in WoltLab Suite Core.
254 The notable parts here are:
255 - The `action` attribute of the `form` element is set depending on which controller will handle the request.
256 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.
257 - The field that caused the validation error can be accessed via `$errorField`.
258 - The type of the validation error can be accessed via `$errorType`.
259 For an empty input field, we show the generic `wcf.global.form.error.empty` language item.
260 In all other cases, we use the error type to determine the object- and property-specific language item to show.
261 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.
262 - Input fields can be grouped into different `.section` elements.
263 At the end of each `.section` element, there should be an template event whose name ends with `Fields`.
264 The first part of the event name should reflect the type of fields in the particular `.section` element.
265 Here, the input fields are just general “data” fields so that the event is called `dataFields`.
266 - After the last `.section` element, fire a `section` event so that plugins can add further sections.
267 - Lastly, the `.formSubmit` shows the submit button and `{csrfToken}` contains a CSRF token that is automatically validated after the form is submitted.
268
269 ### Person Edit Form
270
271 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.
272
273 #### `PersonEditForm`
274
275 {% highlight php %}
276 {% include tutorial/tutorial-series/part-1/files/lib/acp/form/PersonEditForm.class.php %}
277 {% endhighlight %}
278
279 In general, edit forms extend the associated add form so that the code to read and to validate the input data is simply inherited.
280
281 After setting a different active menu item, we declare two new properties for the edited person:
282 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.
283
284 Now let use go through the different methods in chronological order again:
285
286 1. `readParameters()` reads the passed ID of the edited person and creates a `Person` object based on this ID.
287 If the ID is invalid, `$this->person->personID` is `null` and an `IllegalLinkException` is thrown.
288 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.
289 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.
290 1. `save()` handles saving the changed data.
291
292 {% include callout.html content="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!" type="warning" %}
293
294 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.
295 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.
296 Furthermore, we assign the template variable `$action` `edit` as value.
297
298 {% include callout.html content="After calling `parent::assignVariables()`, the template variable `$action` actually has the value `add` so that here, we are overwriting this already assigned value." type="info" %}
299
300
301 ## Frontend
302
303 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.
304 This page should also be directly linked in the main menu.
305
306 ### `page.xml`
307
308 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.html):
309
310 {% highlight xml %}
311 {% include tutorial/tutorial-series/part-1/page.xml %}
312 {% endhighlight %}
313
314 For more information about what each of the elements means, please refer to the [page package installation plugin page](package_pip_page.html).
315
316 ### `menuItem.xml`
317
318 Next, we register the menu item using the [menuItem package installation plugin](package_pip_menuItem.html):
319
320 {% highlight xml %}
321 {% include tutorial/tutorial-series/part-1/menuItem.xml %}
322 {% endhighlight %}
323
324 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.
325
326 ### People List
327
328 As in the ACP, we need a controller and a template.
329 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.
330 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.
331
332 #### `PersonListPage`
333
334 {% highlight php %}
335 {% include tutorial/tutorial-series/part-1/files/lib/page/PersonListPage.class.php %}
336 {% endhighlight %}
337
338 This class is almost identical to the ACP version.
339 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.
340 Furthermore, `$neededPermissions` has not been set because in the front end, users do not need any special permission to access the page.
341 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.
342
343 #### `personList.tpl`
344
345 {% highlight smarty %}
346 {% include tutorial/tutorial-series/part-1/templates/personList.tpl %}
347 {% endhighlight %}
348
349 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.
350 Furthermore, we include a template called `header` before actually showing any of the page contents and terminate the template by including the `footer` template.
351
352 Now, let us take a closer look at the differences:
353
354 - We do not explicitly create a `.contentHeader` element but simply assign the title to the `contentTitle` variable.
355 The value of the assignment is simply the title of the page and a badge showing the number of listed people.
356 The `header` template that we include later will handle correctly displaying the content header on its own based on the `$contentTitle` variable.
357 - Next, we create additional element for the HTML document’s `<head>` element.
358 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).
359 - 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.
360 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.
361 - The main part of the page is the listing of the people.
362 We use a structure similar to the one used for displaying registered users.
363 Here, for each person, we simply display a FontAwesome icon representing a person and show the person’s full name relying on `Person::__toString()`.
364 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.
365
366
367 ## `userGroupOption.xml`
368
369 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.html):
370
371 {% highlight xml %}
372 {% include tutorial/tutorial-series/part-1/userGroupOption.xml %}
373 {% endhighlight %}
374
375 We use the existing `admin.content` user group option category for the permission as the people are “content” (similar the the ACP menu item).
376 As the permission is for administrators only, we set `defaultvalue` to `0` and `admindefaultvalue` to `1`.
377 This permission is only relevant for registered users so that it should not be visible when editing the guest user group.
378 This is achieved by setting `usersonly` to `1`.
379
380
381 ## `package.xml`
382
383 Lastly, we need to create the `package.xml` file.
384 For more information about this kind of file, please refer to [the `package.xml` page](package_package-xml.html).
385
386 {% highlight xml %}
387 {% include tutorial/tutorial-series/part-1/package.xml %}
388 {% endhighlight %}
389
390 As this is a package for WoltLab Suite Core 3, we need to require it using `<requiredpackage>`.
391 We require the latest version (when writing this tutorial) `3.0.0 RC 4`.
392 Additionally, we disallow installation of the package in the next major version `3.1` by excluding the `3.1.0 Alpha 1` version.
393 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.
394
395 The most important part are to installation instructions.
396 First, we install the ACP templates, files and templates, create the database table and import the language item.
397 Afterwards, the ACP menu items and the permission are added.
398 Now comes the part of the instructions where the order of the instructions is crucial:
399 In `menuItem.xml`, we refer to the `com.woltlab.wcf.people.PersonList` page that is delivered by `page.xml`.
400 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!
401
402 ---
403
404 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.
405
406 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).