.zip`.
155 |
156 | ### Customizing the build process
157 |
158 | Aside from just creating the release ZIP there may be additional files you wish to include in your ZIP, other more advanced build processes you want to run such as minifying or concatenating JS or running certain shell commands. All of this can be taken care of in your `build.json` file. This is a typical `build.json` file:
159 |
160 | ```json title="build.json"
161 | {
162 | "additional_files": [
163 | "js/demo/portal"
164 | ],
165 | "minify": [
166 | "js/demo/portal/a.js",
167 | "js/demo/portal/b.js"
168 | ],
169 | "rollup": {
170 | "js/demo/portal/ab-rollup.js": [
171 | "js/demo/portal/a.min.js",
172 | "js/demo/portal/b.min.js"
173 | ]
174 | },
175 | "exec": [
176 | "echo '{title} version {version_string} ({version_id}) has been built successfully!' > 'src/addons/Demo/Portal/_build/built.txt'"
177 | ]
178 | }
179 | ```
180 |
181 | If you have assets, such as JavaScript, which need to be served outside of your add-on directory, you can tell the build process to copy files or directories using the `additional_files` array within `build.json`. During development it isn't always feasible to keep files outside of your add-on directory, so if you prefer, you can keep the files in your add-on `_files` directory instead. When copying the additional files, we will check there first.
182 |
183 | If you ship some JS files with your add-on, you may want to minify those files for performance reasons. You can specify which files you want to minify right inside your `build.json`. You can list these as an array or you can just specify it as `'*'` which will just minify everything in your `js` directory as long as that path has JS files within it after copying the additional files to the build. Any files minified will automatically have a suffix of `.min.js` instead of `.js` and the original files will still be in the package.
184 |
185 | You may prefer to roll up your multiple JS files into a single file. If you do, you can use the `rollup` array to define that. The key is the resulting combined filename, and the items within that array are the paths to the JS files that will be combined into a single file.
186 |
187 | Finally, you may have certain processes that need to be run just before the package is built and finalised. This could be any combination of things. Ultimately, if it is a command that can be run from the shell (including PHP scripts) then you can specify it here. The example above is of course fairly useless, but it does at least demonstrate that certain placeholders can be used. These placeholders are replaced with scalar values you can get from the `XF\AddOn\AddOn` object which is generally any value available in the `addon.json` file, or the `AddOn` entity.
188 |
189 | ## Development commands
190 |
191 | There are actually quite a few development related commands, but only the two most important ones are being covered here.
192 |
193 | To use any of these commands, you must have [development mode](#enabling-development-mode) enabled in your
194 | `config.php` file.
195 |
196 | !!! warning
197 | Both of the following commands can potentially cause data loss if there is a situation whereby the database and `_output`
198 | directory become out of sync. It is always recommended to use a VCS (Version Control System) such as
199 | [GitHub](https://github.com) to mitigate the impact of such mistakes.
200 |
201 | ### Import development output
202 |
203 | ```sh title="Terminal"
204 | php cmd.php xf-dev:import --addon [addon_id]
205 | ```
206 |
207 | Running this command will import all of the development output files from your add-on `_output` directory into the
208 | database.
209 |
210 | ### Export development output
211 |
212 | ```sh title="Terminal"
213 | php cmd.php xf-dev:export --addon [addon_id]
214 | ```
215 |
216 | This will export all data currently associated to your add-on in the database to files within your
217 | `_output` directory.
218 |
219 | ## Debugging code
220 |
221 | It should be possible to set up your favourite debugger tool (XDebug, Zend Debugger etc.) to work with XF2. Though, sometimes, debugging code can be as rudimentary as just quickly seeing what value (or value type) a variable holds at a given time.
222 |
223 | ### Dump a variable
224 |
225 | PHP of course has a tool built-in to handle this. You'll likely know it as `var_dump()`. XF ships with two replacements for this:
226 |
227 | ```php
228 | \XF::dump($var);
229 | \XF::dumpSimple($var);
230 | ```
231 |
232 | The simple version mostly just dumps out the value of a variable in plain text. For example, if you just use it to dump the value of an array, you will see an output at the top of the page like this:
233 |
234 | ```title="Dump"
235 | array(2) {
236 | ["user_id"] => int(1)
237 | ["username"] => string(5) "Admin"
238 | }
239 | ```
240 |
241 | This is actually the same output as a standard var_dump, but slightly modified for readability and wrapped inside `` tags to ensure whitespace is maintained when rendering.
242 |
243 | The alternative is actually a component named VarDumper from the Symfony project. It outputs HTML, CSS and JS to create a much more functional and potentially easier to read output. It allows you to collapse certain sections, and for certain values which can output a considerable amount of data, such as objects, it can collapse those sections automatically.
244 |
--------------------------------------------------------------------------------
/docs/entities-finders-repositories.md:
--------------------------------------------------------------------------------
1 | # Entities, finders and repositories
2 |
3 | There are a number of ways to interact with data within XF2. In XF1 this was mostly geared towards writing out raw SQL statements inside Model files. The approach in XF2 has moved away from this, and we have added a number of new ways in its place. We'll first look at the preferred method for performing database queries - the finder.
4 |
5 | ## The Finder
6 |
7 | We have introduced a new "Finder" system which allows queries to be built up programmatically in a object oriented way so that raw database queries do not need to be written. The Finder system works hand in hand with the Entity system, which we talk about in more detail below. The first argument passed into the finder method is the short class name for the Entity you want to work with. Let's just convert some of the queries mentioned in the section above to use the Finder system instead. For example, to access a single user record:
8 |
9 | ```php
10 | $finder = \XF::finder('XF:User');
11 | $user = $finder->where('user_id', 1)->fetchOne();
12 | ```
13 |
14 | One of the main differences between the direct query approach and using the Finder is that the base unit of data returned by the Finder is not an array. In the case of a Finder object which calls the `fetchOne` method (which only returns a single row from the database), a single Entity object will be returned.
15 |
16 | Let's look at a slightly different approach which will return multiple rows:
17 |
18 | ```php
19 | $finder = \XF::finder('XF:User');
20 | $users = $finder->limit(10)->fetch();
21 | ```
22 |
23 | This example will query 10 records from the xf_user table, and it will return them as an `ArrayCollection` object. This is a special object which acts similarly to an array, in that it is traversable (you can loop through it) and it has some special methods that can tell you the total number of entries it has, grouping by certain values, or other array like operations such as filtering, merging, getting the first or last entry etc.
24 |
25 | Finder queries generally should be expected to retrieve all columns from a table, so there's no specific equivalent to fetch only certain values certain columns.
26 |
27 | Instead, to get a single value, you would just fetch one entity and read the value directly from that:
28 |
29 | ```php
30 | $finder = \XF::finder('XF:User');
31 | $username = $finder->where('user_id', 1)->fetchOne()->username;
32 | ```
33 |
34 | Similarly, to get an array of values from a single column, you can use the `pluckFrom` method:
35 |
36 | ```php
37 | $finder = \XF::finder('XF:User');
38 | $usernames = $finder->limit(10)->pluckFrom('username')->fetch();
39 | ```
40 |
41 | So far we've seen the Finder apply somewhat simple where and limit constraints. So let's look at the Finder in more detail, including a bit more detail about the `where` method itself.
42 |
43 | ### where method
44 |
45 | The `where` method can support up to three arguments. The first being the condition itself, e.g. the column you are querying. The second would ordinarily be the operator. The third is the value being searched for. If you supply only two arguments, as you have seen above, then it automatically implies the operator is `=`. Below is a list of the other operators which are valid:
46 |
47 | - `=`
48 | - `<>`
49 | - `!=`
50 | - `>`
51 | - `>=`
52 | - `<`
53 | - `<=`
54 | - `LIKE`
55 | - `BETWEEN`
56 |
57 | So, we could get a list of the valid users who registered in the last 7 days:
58 |
59 | ```php
60 | $finder = \XF::finder('XF:User');
61 | $users = $finder->where('user_state', 'valid')->where('register_date', '>=', time() - 86400 * 7)->fetch();
62 | ```
63 |
64 | As you can see you can call the `where` method as many times as you like, but in addition to that, you can choose to pass in an array as the only argument of the method, and build up your conditions in a single call. The array method supports two types, both of which we can use on the query we built above:
65 |
66 | ```php
67 | $finder = \XF::finder('XF:User');
68 | $users = $finder->where([
69 | 'user_state' => 'valid',
70 | ['register_date', '>=', time() - 86400 * 7]
71 | ])
72 | ->fetch();
73 | ```
74 |
75 | It wouldn't usually be recommended or clear to mix the usage like this, but it does demonstrate the flexibility of the method somewhat. Now that the conditions are in an array, we can either specify the column name (as the array key) and value for an implied `=` operator or we can actually define another array containing the column, operator and value.
76 |
77 | ### whereOr method
78 |
79 | With the above examples, both conditions need to be met, i.e. each condition is joined by the `AND` operator. However, sometimes it is necessary to only meet part of your condition, and this is possible by using the `whereOr` method. For example, if you wanted to search for users who are either not valid or have posted zero messages, you can build that as follows:
80 |
81 | ```php
82 | $finder = \XF::finder('XF:User');
83 | $users = $finder->whereOr(
84 | ['user_state', '<>', 'valid'],
85 | ['message_count', 0]
86 | )->fetch();
87 | ```
88 |
89 | Similar to the example in the previous section, as well as passing up to two conditions as separate arguments, you can also just pass an array of conditions to the first argument:
90 |
91 | ```php
92 | $finder = \XF::finder('XF:User');
93 | $users = $finder->whereOr([
94 | ['user_state', '<>', 'valid'],
95 | ['message_count', 0],
96 | ['is_banned', 1]
97 | ])->fetch();
98 | ```
99 |
100 | ### with method
101 |
102 | The `with` method is essentially equivalent to using the `INNER|LEFT JOIN` syntax, though it relies upon the Entity having had its "Relations" defined. We won't go into that until the next page, but this should just give you an understanding of how it works. Let's now use the Thread finder to retrieve a specific thread:
103 |
104 | ```php
105 | $finder = \XF::finder('XF:Thread');
106 | $thread = $finder->with('Forum', true)->where('thread_id', 123)->fetchOne();
107 | ```
108 |
109 | This query will fetch the Thread entity where the `thread_id = 123` but it will also do a join with the xf_forum table, behind the scenes. In terms of controlling how to do an `INNER JOIN` rather than a `LEFT JOIN`, that is what the second argument is for. In this case we've set the "must exist" argument to true, so it will flip the join syntax to using `INNER` rather than the default `LEFT`.
110 |
111 | We'll go into more detail about how to access the data fetched from this join in the next section.
112 |
113 | It's also possible to pass an array of relations into the `with` method to do multiple joins.
114 |
115 | ```php
116 | $finder = \XF::finder('XF:Thread');
117 | $thread = $finder->with(['Forum', 'User'], true)->where('thread_id', 123)->fetchOne();
118 | ```
119 |
120 | This would join to the xf_user table to get the thread author too. However, with the second argument there still being `true`, we might not need to do an `INNER` join for the user join, so, we could just chain the methods instead:
121 |
122 | ```php
123 | $finder = \XF::finder('XF:Thread');
124 | $thread = $finder->with('Forum', true)->with('User')->where('thread_id', 123)->fetchOne();
125 | ```
126 |
127 | ### order, limit and limitByPage methods
128 |
129 | #### order method
130 |
131 | This method allows you to modify your query so the results are fetched in a specific order. It takes two arguments, the first is the column name, and the second is, optionally, the direction of the sort. So, if you wanted to list the 10 users who have the most messages, you could build the query like this:
132 |
133 | ```php
134 | $finder = \XF::finder('XF:User');
135 | $users = $finder->order('message_count', 'DESC')->limit(10);
136 | ```
137 |
138 | !!! note
139 | Now is probably a good time to mention that finder methods can mostly be called in any order. For example: `$threads = $finder->limit(10)->where('thread_id', '>', 123)->order('post_date')->with('User')->fetch();`
140 | Although if you wrote a MySQL query in that order you'd certainly encounter some syntax issues, the Finder system will still build it all in the correct order and the above code, although odd looking and probably not recommended, is perfectly valid.
141 |
142 | As with a standard MySQL query, it is possible to order a result set on multiple columns. To do that, you can just call the order method again. It's also possible to pass multiple order clauses into the order method using an array.
143 |
144 | ```php
145 | $finder = \XF::finder('XF:User');
146 | $users = $finder->order('message_count', 'DESC')->order('register_date')->limit(10);
147 | ```
148 |
149 | #### limit method
150 |
151 | We've already seen how to limit a query to a specific number of records being returned:
152 |
153 | ```php
154 | $finder = \XF::finder('XF:User');
155 | $users = $finder->limit(10)->fetch();
156 | ```
157 |
158 | However, there's actually an alternative to calling the limit method directly:
159 |
160 | ```php
161 | $finder = \XF::finder('XF:User');
162 | $users = $finder->fetch(10);
163 | ```
164 |
165 | It's possible to pass your limit directly into the `fetch()` method. It's also worth noting that the `limit` (and `fetch`) method supports two arguments. The first obviously being the limit, the second being the offset.
166 |
167 | ```php
168 | $finder = \XF::finder('XF:User');
169 | $users = $finder->limit(10, 100)->fetch();
170 | ```
171 |
172 | The offset value here essentially means the first 100 results will be discarded, and the first 10 after that will be returned. This kind of approach is useful for providing paginated results, though we actually also have an easier way to do that...
173 |
174 | #### limitByPage method
175 |
176 | This method is a sort of helper method which ultimately sets the appropriate limit and offset based on the "page" you're currently viewing and how many "per page" you require.
177 |
178 | ```php
179 | $finder = \XF::finder('XF:User');
180 | $users = $finder->limitByPage(3, 20);
181 | ```
182 |
183 | In this case, the limit is going to be set to 20 (which is our per page value) and the offset is going to be set to 40 because we're starting on page 3.
184 |
185 | Occasionally, it is necessary for us to grab additional more data than the limit. Over-fetching can be useful to help detect whether you have additional data to display after the current page, or if you have a need to filter the initial result set down based on permissions. We can do that with the third argument:
186 |
187 | ```php
188 | $finder = \XF::finder('XF:User');
189 | $users = $finder->limitByPage(3, 20, 1);
190 | ```
191 |
192 | This will get a total of up to **21** users (20 + 1) starting at page 3.
193 |
194 | ### getQuery method
195 |
196 | When you first start working with the finder, as intuitive as it is, you may occasionally wonder whether you're using it correctly, and whether it is going to build the query you expect it to. We have a method named `getQuery` which can tell us the current query that will be built with the current finder object. For example:
197 |
198 | ```php
199 | $finder = \XF::finder('XF:User')
200 | ->where('user_id', 1);
201 |
202 | \XF::dumpSimple($finder->getQuery());
203 | ```
204 |
205 | This will output something similar to:
206 |
207 | ```title="Dump"
208 | string(67) "SELECT `xf_user`.*
209 | FROM `xf_user`
210 | WHERE (`xf_user`.`user_id` = 1)"
211 | ```
212 |
213 | You probably won't need it very often, but it can be useful if the finder isn't quite returning the results you expected. Read more about the `dumpSimple` method in the [Dump a variable](development-tools.md#dump-a-variable) section.
214 |
215 | ### Custom finder methods
216 |
217 | So far we have seen the finder object get setup with an argument similar to `XF:User` and `XF:Thread`. For the most part, this identifies the Entity class the finder is working with and will resolve to, for example, `XF\Entity\User`. However, it can additionally represent a finder class. Finder classes are optional, but they serve as a way to add custom finder methods to specific finder types. To see this in action, let's look at the finder class that relates to `XF:User` which can be found in the `XF\Finder\User` class.
218 |
219 | Here's an example finder method from that class:
220 |
221 | ```php
222 | public function isRecentlyActive($days = 180)
223 | {
224 | $this->where('last_activity', '>', time() - ($days * 86400));
225 | return $this;
226 | }
227 | ```
228 |
229 | What this allows us to do is to now call that method on any User finder object. So if we take an example earlier:
230 |
231 | ```php
232 | $finder = \XF::finder('XF:User');
233 | $users = $finder->isRecentlyActive(20)->order('message_count', 'DESC')->limit(10);
234 | ```
235 |
236 | This query, which earlier just returned 10 users in descending message count order, will now return the 10 users in that order who have been recently active in the last 20 days.
237 |
238 | Even though for a lot of entity types a finder class doesn't exist, it is still possible to extend these non existent classes in the same way as mentioned in the [Extending classes](development-tools.md#extending-classes) section.
239 |
240 | ## The Entity system
241 |
242 | If you're familiar with XF1, you may be familiar with some of the concepts behind Entities because they have ultimately derived from the DataWriter system there. In case you're not so familiar with them, the following section should give you some idea.
243 |
244 | ### Entity structure
245 |
246 | The `Structure` object consists of a number of properties which define the structure of the Entity and the database table it relates to. The structure object itself is setup inside the entity it relates to. Let's look at some of the common properties from the User entity:
247 |
248 | #### Table
249 |
250 | ```php
251 | $structure->table = 'xf_user';
252 | ```
253 |
254 | This tells the Entity which database table to use when updating and inserting records, and also tells the Finder which table to read from when building queries to execute. Additionally, it plays a part in knowing which other tables your query needs to join to.
255 |
256 | #### Short name
257 |
258 | ```php
259 | $structure->shortName = 'XF:User';
260 | ```
261 |
262 | This is the just the short class name of both the Entity itself and the Finder class (if applicable).
263 |
264 | #### Content type
265 |
266 | ```php
267 | $structure->contentType = 'user';
268 | ```
269 |
270 | This defines what content type this Entity represents. This will not be needed in most entity structures. It is used to connect to specific things used by the "content type" system (which will be covered in another section).
271 |
272 | #### Primary key
273 |
274 | ```php
275 | $structure->primaryKey = 'user_id';
276 | ```
277 |
278 | Defines the column which represents the primary key in the database table. If a table supports more than a single column as a primary key, then this can be defined as an array.
279 |
280 | #### Columns
281 |
282 | ```php
283 | $structure->columns = [
284 | 'user_id' => ['type' => self::UINT, 'autoIncrement' => true, 'nullable' => true, 'changeLog' => false],
285 | 'username' => ['type' => self::STR, 'maxLength' => 50,
286 | 'required' => 'please_enter_valid_name'
287 | ]
288 | // and many more columns ...
289 | ];
290 | ```
291 |
292 | This is a key part of the configuration of the entity as this goes into a lot of detail to explain the specifics of each database column that the Entity is responsible for. This tells us the type of data that is expected, whether a value is required, what format it should match, whether it should be a unique value, what its default value is, and much more.
293 |
294 | Based on the `type`, the entity manager knows whether to encode or decode a value in a certain way. This may be a somewhat simple process of casting a value to a string or an integer, or slightly more complicated such as using `json_encode()` on an array when writing to the database or using `json_decode()` on a JSON string when reading from the database so that the value is correctly returned to the entity object as an array without us needing to manually do that. It can also support comma separated values being encoded/decoded appropriately.
295 |
296 | Occasionally it is necessary to do some additional verification or modification of a value before it is written. As an example, in the User entity, look at the `verifyStyleId()` method. When a value is set on the `style_id` field, we automatically check to see if a method named `verifyStyleId()` exists, and if it does, we run the value through that first.
297 |
298 | #### Behaviors
299 |
300 | ```php
301 | $structure->behaviors = [
302 | 'XF:ChangeLoggable' => []
303 | ];
304 | ```
305 |
306 | This is an array of behavior classes which should be used by this entity. Behavior classes are a way of allowing certain code to be reused generically across multiple entity types (only when the entity changes, not on reads). A good example of this is the `XF:Likeable` behavior which is able to automatically execute certain actions on entities which support content which can be "liked". This includes automatically recalculating counts when visibility changes occur within the content and automatically deleting likes when the content is deleted.
307 |
308 | #### Getters
309 |
310 | ```php
311 | $structure->getters = [
312 | 'is_super_admin' => true,
313 | 'last_activity' => true
314 | ];
315 | ```
316 |
317 | Getter methods are automatically called when the named fields are called. For example, if we request `is_super_admin` from a User entity, this will automatically check for, and use the `getIsSuperAdmin()` method. The interesting thing to note about this is that the `xf_user` table doesn't actually have a field named `is_super_admin`. This actually exists on the Admin entity, but we have added it as a getter method as a shorthand way of accessing that value. Getter methods can also be used to override the values of existing fields directly, which is the case for the `last_activity` value here. `last_activity` is actually a cached value which is updated usually when a user logs out. However, we store the user's latest activity date in the xf_session_activity table, so we can use this `getLastActivity` method to return that value instead of the cached last activity value. Should you ever have a need to bypass the getter method entirely, and just get the true entity value, just suffix the column name with an underscore, e.g. `$user->last_activity\_`.
318 |
319 | Because an entity is just like any other PHP object, you can add more methods to them. A common use case for this is for adding things like permission check methods that can be called on the entity itself.
320 |
321 | #### Relations
322 |
323 | ```php
324 | $structure->relations = [
325 | 'Admin' => [
326 | 'entity' => 'XF:Admin',
327 | 'type' => self::TO_ONE,
328 | 'conditions' => 'user_id',
329 | 'primary' => true
330 | ]
331 | ];
332 | ```
333 |
334 | This is how Relations are defined. What are relations? They define the relationship between entities which can be used to perform join queries to other tables or fetch records associated to an entity on the fly. If we remember the `with` method on the finder, if we wanted to fetch a specific user and preemptively fetch the user's Admin record (if it exists) then we would do something like the following:
335 |
336 | ```php
337 | $finder = \XF::finder('XF:User');
338 | $user = $finder->where('user_id', 1)->with('Admin')->fetchOne();
339 | ```
340 |
341 | This will use the information defined in the user entity for the `Admin` relation and the details of the `XF:Admin` entity structure to know that this user query should perform a `LEFT JOIN` on the xf_admin table and the `user_id` column. To access the admin last login date from the user entity:
342 |
343 | ```php
344 | $lastLogin = $user->Admin->last_login; // returns timestamp of the last admin login
345 | ```
346 |
347 | However, it's not always necessary to do a join in a finder to get related information for an entity. For example, if we take the above example without the `with` method call:
348 |
349 | ```php
350 | $finder = \XF::finder('XF:User');
351 | $user = $finder->where('user_id', 1)->fetchOne();
352 | $lastLogin = $user->Admin->last_login; // returns timestamp of the last admin login
353 | ```
354 |
355 | We still get the `last_login` value here. It does this by performing the additional query to get the Admin entity on the fly.
356 |
357 | The example above uses the `TO_ONE` type, and this relation, therefore, relates one entity to one other entity. We also have a `TO_MANY` type.
358 |
359 | It is not possible to fetch an entire `TO_MANY` relation (e.g. with a join / `with` method on the finder), but at the cost of a query it is possible to read that at any time on the fly, such as in the final `last_login` example above.
360 |
361 | One such relation that is defined on the User entity is the `ConnectedAccounts` relation:
362 |
363 | ```php
364 | $structure->relations = [
365 | 'ConnectedAccounts' => [
366 | 'entity' => 'XF:UserConnectedAccount',
367 | 'type' => self::TO_MANY,
368 | 'conditions' => 'user_id',
369 | 'key' => 'provider'
370 | ]
371 | ];
372 | ```
373 |
374 | This relation is able to return the records from the xf_user_connected_account table that match the current user ID as a `FinderCollection`. This is similar to the `ArrayCollection` object we mentioned in [The Finder](#the-finder) section above. The relation definition specifies that the collection should be keyed by the `provider` field.
375 |
376 | Although it isn't possible to fetch multiple records while performing a finder query, it is possible to use a `TO_MANY` relation to fetch a **single** record from that relation. As an example, if we wanted to see if the user was associated to a specific connected account provider, we can at least fetch that while querying:
377 |
378 | ```php
379 | $finder = \XF::finder('XF:User');
380 | $user = $finder->where('user_id', 1)->with('ConnectedAccounts|facebook')->fetchOne();
381 | ```
382 |
383 | #### Options
384 |
385 | ```php
386 | $structure->options = [
387 | 'custom_title_disallowed' => preg_split('/\r?\n/', $options->disallowedCustomTitles),
388 | 'admin_edit' => false,
389 | 'skip_email_confirm' => false
390 | ];
391 | ```
392 |
393 | Entity options are a way of modifying the behavior of the entity under certain conditions. For example, if we set `admin_edit` to true (which is the case when editing a user in the Admin CP), then certain checks will be skipped such as to allow a user's email address to be empty.
394 |
395 | ### The Entity life cycle
396 |
397 | The Entity plays a significant job in terms of managing the life cycle of a record within the database. As well as reading values from it, and writing values to it, the Entity can be used to delete records and trigger certain events when all of these actions occur so that certain tasks can be performed, or certain associated records can be updated as well. Let's look at some of these events that happen when an entity is saving:
398 |
399 | - `_preSave()` - This happens before the save process begins, and is primarily used to perform any additional pre-save validations or to set additional data before the save happens.
400 | - `_postSave()` - After the data has been saved, but before any transactions are committed, this method is called and you can use it to perform any additional work that should trigger after an entity has been saved.
401 |
402 | There are additionally `_preDelete()` and `_postDelete()` which work in a similar way, but when a delete is happening.
403 |
404 | The Entity is also able to give information on its current state. For example, there is an `isInsert()` and `isUpdate()` method so you can detect whether this is a new record being inserted or an existing record being updated. There is an `isChanged()` method which can tell you whether a specific field has changed since the last save.
405 |
406 | Let's look at some real examples of these methods in action, in the User entity.
407 |
408 | ```php
409 | protected function _preSave()
410 | {
411 | if ($this->isChanged('user_group_id') || $this->isChanged('secondary_group_ids'))
412 | {
413 | $groupRepo = $this->getUserGroupRepo();
414 | $this->display_style_group_id = $groupRepo->getDisplayGroupIdForUser($this);
415 | }
416 |
417 | // ...
418 | }
419 |
420 | protected function _postSave()
421 | {
422 | // ...
423 |
424 | if ($this->isUpdate() && $this->isChanged('username') && $this->getExistingValue('username') != null)
425 | {
426 | $this->app()->jobManager()->enqueue('XF:UserRenameCleanUp', [
427 | 'originalUserId' => $this->user_id,
428 | 'originalUserName' => $this->getExistingValue('username'),
429 | 'newUserName' => $this->username
430 | ]);
431 | }
432 |
433 | // ...
434 | ```
435 |
436 | In the `_preSave()` example we fetch and cache the new display group ID for a user based on their changed user groups. In the `_postSave()` example, we trigger a job to run after a user's name has been changed.
437 |
438 | ## Repositories
439 |
440 | Repositories are a new concept for XF2, but you might not be blamed for comparing them to the "Model" objects from XF1. We don't have a model object in XF2 because we have much better places and ways to fetch and write data to the database. So, rather than having a massive class which contains all of the queries your add-on needs, and all of the various different ways to manipulate those queries, we have the finder which adds a lot more flexibility.
441 |
442 | It's also worth bearing in mind that in XF1 the Model objects were a bit of a "dumping ground" for so many things. Many of which are now redundant. For example, in XF1 all of the permission rebuilding code was in the permission model. In XF2, we have specific services and objects which handle this.
443 |
444 | So, what are Repositories? They correspond with an entity and a finder and hold methods which generally return a finder object setup for a specific purpose. Why not just return the result of the finder query? Well, if we return the finder object itself then it serves as a useful extension point for add-ons to extend that and modify the finder object before the entity or collection is returned.
445 |
446 | Repositories may also contain some specific methods for things like cache rebuilding.
447 |
--------------------------------------------------------------------------------
/docs/files/Demo-Portal-1.0.0 Alpha.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/Demo-Portal-1.0.0 Alpha.zip
--------------------------------------------------------------------------------
/docs/files/example-sources/all-for-one-criterion-2.0.10.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/example-sources/all-for-one-criterion-2.0.10.zip
--------------------------------------------------------------------------------
/docs/files/example-sources/posts-remover-2.0.10.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/example-sources/posts-remover-2.0.10.zip
--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-awarded.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-awarded.png
--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-notice.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-notice.png
--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-type-messages-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-type-messages-after.png
--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-type-messages-before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-type-messages-before.png
--------------------------------------------------------------------------------
/docs/files/images/example-custom-criteria-type-remover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-custom-criteria-type-remover.png
--------------------------------------------------------------------------------
/docs/files/images/example-userbanners-tag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/example-userbanners-tag.png
--------------------------------------------------------------------------------
/docs/files/images/helper_criteria_tabs_example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/helper_criteria_tabs_example.png
--------------------------------------------------------------------------------
/docs/files/images/linux-debugging.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/linux-debugging.jpg
--------------------------------------------------------------------------------
/docs/files/images/linux-php-versions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/linux-php-versions.png
--------------------------------------------------------------------------------
/docs/files/images/macos-debugging.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/macos-debugging.jpg
--------------------------------------------------------------------------------
/docs/files/images/macos-php-versions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/macos-php-versions.png
--------------------------------------------------------------------------------
/docs/files/images/server-report.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/images/server-report.png
--------------------------------------------------------------------------------
/docs/files/info.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xenforo-ltd/docs/fa8a30ab2b6d084dae262de83e870c9caceffc03/docs/files/info.zip
--------------------------------------------------------------------------------
/docs/files/linux/install-debian.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # PHP versions for Debian
4 | sudo apt-get -y install apt-transport-https lsb-release ca-certificates curl
5 | sudo curl -sSL -o /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
6 | sudo sh -c 'echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" > /etc/apt/sources.list.d/php.list'
7 |
8 | # TablePlus (optional)
9 | wget -O - -q http://deb.tableplus.com/apt.tableplus.com.gpg.key | sudo apt-key add -
10 | sudo add-apt-repository "deb [arch=amd64] https://deb.tableplus.com/debian tableplus main"
11 |
12 | # ElasticSearch (optional)
13 | wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
14 | echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
15 |
16 | # Fetch the latest version info and upgrade existing packages
17 | sudo apt update -y
18 | sudo apt upgrade -y
19 |
20 | # PHP stuff
21 | sudo apt install php5.6-fpm -y
22 | sudo apt install php7.4-fpm -y
23 | sudo apt install php8.0-fpm -y
24 | sudo apt install php-pear -y
25 | sudo apt install php-memcache -y
26 |
27 | # PHP modules
28 | for module in xdebug imagick gettext gd bcmath bz2 curl dba xml gmp intl ldap mbstring mysql odbc soap zip enchant sqlite3
29 | do
30 | for version in 7.4 5.6 8.0
31 | do
32 | sudo apt install php${version}-${module} -y
33 | done
34 | done
35 |
36 | # Apache web server
37 | sudo apt install apache2 -y
38 | # enable Apache FastCGI / FPM module
39 | sudo a2enmod proxy_fcgi
40 |
41 | # TablePlus (optional)
42 | sudo apt install tableplus -y
43 |
44 | # ElasticSearch (optional)
45 | sudo apt install elasticsearch -y
46 |
47 | # MariaDB (MySQL)
48 | sudo apt install mariadb-server -y
49 | sudo mysql -uroot -p
--------------------------------------------------------------------------------
/docs/files/linux/install-ubuntu.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # PHP versions for Ubuntu
4 | sudo add-apt-repository -y ppa:ondrej/php
5 |
6 | # TablePlus (optional)
7 | wget -O - -q http://deb.tableplus.com/apt.tableplus.com.gpg.key | sudo apt-key add -
8 | sudo add-apt-repository "deb [arch=amd64] https://deb.tableplus.com/debian tableplus main"
9 |
10 | # ElasticSearch (optional)
11 | wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
12 | echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
13 |
14 | # Fetch the latest version info and upgrade existing packages
15 | sudo apt update -y
16 | sudo apt upgrade -y
17 |
18 | # PHP stuff
19 | sudo apt install php5.6-fpm -y
20 | sudo apt install php7.4-fpm -y
21 | sudo apt install php8.0-fpm -y
22 | sudo apt install php-pear -y
23 | sudo apt install php-memcache -y
24 |
25 | # PHP modules
26 | for module in xdebug imagick gettext gd bcmath bz2 curl dba xml gmp intl ldap mbstring mysql odbc soap zip enchant sqlite3
27 | do
28 | for version in 7.4 5.6 8.0
29 | do
30 | sudo apt install php${version}-${module} -y
31 | done
32 | done
33 |
34 | # Apache web server
35 | sudo apt install apache2 -y
36 | # enable Apache FastCGI / FPM module
37 | sudo a2enmod proxy_fcgi
38 |
39 | # TablePlus (optional)
40 | sudo apt install tableplus -y
41 |
42 | # ElasticSearch (optional)
43 | sudo apt install elasticsearch -y
44 |
45 | # MariaDB (MySQL)
46 | sudo apt install mariadb-server -y
47 | sudo mysql_secure_installation
48 |
--------------------------------------------------------------------------------
/docs/files/linux/php56/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | listen = 127.0.0.1:9056
--------------------------------------------------------------------------------
/docs/files/linux/php56/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension = "xdebug.so"
2 | xdebug.remote_enable = 1
3 | xdebug.remote_connect_back = 1
4 | xdebug.remote_port = 9000
--------------------------------------------------------------------------------
/docs/files/linux/php74/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | listen = 127.0.0.1:9074
--------------------------------------------------------------------------------
/docs/files/linux/php74/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension = "xdebug.so"
2 | xdebug.mode = "debug,develop"
3 | xdebug.discover_client_host = 1
4 | xdebug.client_port = 9000
--------------------------------------------------------------------------------
/docs/files/linux/php80/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | listen = 127.0.0.1:9080
--------------------------------------------------------------------------------
/docs/files/linux/php80/xdebug.ini:
--------------------------------------------------------------------------------
1 | zend_extension = "xdebug.so"
2 | xdebug.mode = "debug,develop"
3 | xdebug.discover_client_host = 1
4 | xdebug.client_port = 9000
--------------------------------------------------------------------------------
/docs/files/macos/httpd/httpd-dev.conf:
--------------------------------------------------------------------------------
1 | User kier
2 | Group staff
3 |
4 | Listen 80
5 | ServerName localhost
6 | Timeout 3600
7 |
8 | LoadModule vhost_alias_module lib/httpd/modules/mod_vhost_alias.so
9 | LoadModule rewrite_module lib/httpd/modules/mod_rewrite.so
10 | LoadModule deflate_module lib/httpd/modules/mod_deflate.so
11 | LoadModule mime_magic_module lib/httpd/modules/mod_mime_magic.so
12 | LoadModule expires_module lib/httpd/modules/mod_expires.so
13 | LoadModule proxy_module lib/httpd/modules/mod_proxy.so
14 | LoadModule proxy_http_module lib/httpd/modules/mod_proxy_http.so
15 | LoadModule proxy_fcgi_module lib/httpd/modules/mod_proxy_fcgi.so
16 |
17 |
18 | DirectoryIndex index.html index.php
19 |
20 |
21 |
22 | DocumentRoot "/Users/kier/Documents/www"
23 |
24 |
25 | Options Indexes FollowSymLinks
26 | AllowOverride all
27 | Require all granted
28 |
29 |
30 |
31 | SetHandler "proxy:fcgi://localhost:9080"
32 |
33 |
--------------------------------------------------------------------------------
/docs/files/macos/php56/htaccess.txt:
--------------------------------------------------------------------------------
1 |
2 | SetHandler "proxy:fcgi://localhost:9056"
3 |
--------------------------------------------------------------------------------
/docs/files/macos/php56/php-dev.ini:
--------------------------------------------------------------------------------
1 | post_max_size = 20M
2 | upload_max_filesize = 10M
3 | date.timezone = UTC
4 |
5 | [mailhog]
6 | smtp_port = 1025
7 | sendmail_path = "/usr/local/bin/mhsendmail"
8 |
9 | [xdebug]
10 | zend_extension = "xdebug.so"
11 | xdebug.remote_enable = 1
12 | xdebug.remote_connect_back = 1
13 | xdebug.remote_port = 9000
14 |
15 | [imagick]
16 | extension = "imagick.so"
--------------------------------------------------------------------------------
/docs/files/macos/php56/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | group = staff
3 | listen = 127.0.0.1:9056
--------------------------------------------------------------------------------
/docs/files/macos/php74/htaccess.txt:
--------------------------------------------------------------------------------
1 |
2 | SetHandler "proxy:fcgi://localhost:9074"
3 |
--------------------------------------------------------------------------------
/docs/files/macos/php74/php-dev.ini:
--------------------------------------------------------------------------------
1 | post_max_size = 20M
2 | upload_max_filesize = 10M
3 | date.timezone = UTC
4 |
5 | [mailhog]
6 | smtp_port = 1025
7 | sendmail_path = "/usr/local/bin/mhsendmail"
8 |
9 | [xdebug]
10 | zend_extension = "xdebug.so"
11 | xdebug.mode = "debug,develop"
12 | xdebug.discover_client_host = 1
13 | xdebug.client_port = 9000
14 |
15 | [imagick]
16 | extension = "imagick.so"
--------------------------------------------------------------------------------
/docs/files/macos/php74/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | group = staff
3 | listen = 127.0.0.1:9074
--------------------------------------------------------------------------------
/docs/files/macos/php80/htaccess.txt:
--------------------------------------------------------------------------------
1 |
2 | SetHandler "proxy:fcgi://localhost:9080"
3 |
--------------------------------------------------------------------------------
/docs/files/macos/php80/php-dev.ini:
--------------------------------------------------------------------------------
1 | post_max_size = 20M
2 | upload_max_filesize = 10M
3 | date.timezone = UTC
4 |
5 | [mailhog]
6 | smtp_port = 1025
7 | sendmail_path = "/usr/local/bin/mhsendmail"
8 |
9 | [xdebug]
10 | zend_extension = "xdebug.so"
11 | xdebug.mode = "debug,develop"
12 | xdebug.discover_client_host = 1
13 | xdebug.client_port = 9000
14 |
15 | [imagick]
16 | extension = "imagick.so"
--------------------------------------------------------------------------------
/docs/files/macos/php80/x.conf:
--------------------------------------------------------------------------------
1 | user = kier
2 | group = staff
3 | listen = 127.0.0.1:9080
--------------------------------------------------------------------------------
/docs/files/scotchbox/Vagrantfile:
--------------------------------------------------------------------------------
1 | # -*- mode: ruby -*-
2 | # vi: set ft=ruby :
3 |
4 | $bootstrap = <