Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 52 additions & 87 deletions en/extending-modx/custom-users.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,29 @@ _old_uri: "2.x/developing-in-modx/advanced-development/extending-moduser"

## Intended Audience

For developers who need extra data on their MODX users and extra functionality on the related classes. The steps below are technical and rely on xPDO: familiarity with xPDO objects and methods (e.g. [getObject](extending-modx/xpdo/class-reference/xpdo/xpdo.getobject "xPDO.getObject")) is required.
Extra user data can sit in its own table with a foreign key to `modx_users`. This page is the deeper path: extend the core `modUser` class and add functionality on the related classes.

The quick way out is a separate table with a foreign key back to `modx_users`. This article covers the deeper integration instead: extending the core `modUser` class.
The steps use xPDO. You need objects and methods such as [getObject](extending-modx/xpdo/class-reference/xpdo/xpdo.getobject "xPDO.getObject").

### See Also

- [Reverse Engineer xPDO Classes from Existing Database Table](extending-modx/xpdo/custom-models/generating-the-model/reverse-engineer "Reverse Engineer xPDO Classes from Existing Database Table")
- [More Examples of xPDO XML Schema Files](extending-modx/xpdo/custom-models/defining-a-schema/more-examples "More Examples of xPDO XML Schema Files")
- [Generating the Model Code](extending-modx/xpdo/custom-models/generating-the-model "Generating the Model Code")



## The Rules

Extending `modUser` does **not** mean writing to the `modx_users` table. You add a separate related table and bind it to the original table by a foreign key. Never replace the `modUser` class outright: build on it.

The only trace of an extended user is its `class_key` — changed from `MODX\Revolution\modUser` (the default in 3.x; `modUser` in 2.x) to the extended class name.


Key rules for extending `modUser`:

1. Use your extension only to access your extra data. Do not interact with user objects that have not been extended.
2. Do not replicate existing `modUser` functionality. Use the built-in user handling instead.
3. Learn `modUser` and `modPrincipal` before coding. Some of their methods (attributes, for one) work per context or resource.
4. Work with attributes through `modPrincipal::loadAttributes()` / `getAttributes()`. They are driven by the `principal_targets` system setting.
5. Access `modUser` methods through `modUser` suggestions.
5. Call `modUser` methods on the user object.

## Steps to extend modUser

Expand All @@ -49,23 +46,21 @@ We add a single extra attribute (e.g. a _facebook\_url_) to the user data. In th
``` xml
<?xml version="1.0" encoding="UTF-8"?>
<model package="extendeduser" baseClass="xPDOObject" platform="mysql" defaultEngine="MyISAM" tablePrefix="ext_">
<!-- extend the modUser class -->
<object class="extUser" extends="modUser">
<composite alias="Data" local="id" class="Userdata" foreign="userdata_id" cardinality="one" owner="local" />
</object>
<object class="Userdata" table="data" extends="xPDOSimpleObject">
<field key="userdata_id" dbtype="int" precision="11" phptype="integer" null="false" attributes="unsigned"/>
<field key="facebook_url" dbtype="varchar" precision="100" phptype="string" null="true" />
<index alias="userdata_id" name="userdata_id" primary="false" unique="true" type="BTREE">
<column key="userdata_id" length="" collation="A" null="false" />
</index>
<aggregate alias="extUser" local="userdata_id" foreign="id" cardinality="one" owner="foreign" />
</object>
</model>
<!-- extend the modUser class -->
<object class="extUser" extends="modUser">
<composite alias="Data" local="id" class="Userdata" foreign="userdata_id" cardinality="one" owner="local" />
</object>
<object class="Userdata" table="data" extends="xPDOSimpleObject">
<field key="userdata_id" dbtype="int" precision="11" phptype="integer" null="false" attributes="unsigned"/>
<field key="facebook_url" dbtype="varchar" precision="100" phptype="string" null="true" />
<index alias="userdata_id" name="userdata_id" primary="false" unique="true" type="BTREE">
<column key="userdata_id" length="" collation="A" null="false" />
</index>
<aggregate alias="extUser" local="userdata_id" foreign="id" cardinality="one" owner="foreign" />
</object>
</model>
```

The extension of modUser happens within a single _object_ node.

#### More Involved Example

Deprecated: `index="unique"` — declare indexes in their own node instead:
Expand Down Expand Up @@ -118,9 +113,21 @@ class extUser extends modUser {
?>
```

### 3. Create (or edit) extension_packages in System Settings
### 3. Register the model

In System settings (System menu), search for [extension_packages](building-sites/settings/extension_packages "extension_packages").
Create a `MODX\Revolution\modExtensionPackage` row. The namespace `extendeduser` must already exist: the loader joins that row and passes the namespace name to `addPackage`. Leave `path` empty to use the namespace directory. MODX appends `model/` when it loads the row. `table_prefix` is `ext_`, the same prefix as the schema on this page.

``` php
$package = $modx->newObject(\MODX\Revolution\modExtensionPackage::class);
$package->fromArray([
'namespace' => 'extendeduser',
'name' => 'extendeduser',
'table_prefix' => 'ext_',
]);
$package->save();
```

The system setting [extension_packages](building-sites/settings/extension_packages "extension_packages") is still read. Its docblock marks it deprecated. In that JSON the `path` already ends in `model/`, because the old loader does not append `model/` itself. Do not copy that path into the `modExtensionPackage` row.

**If the key already exists**, add inside the JSON array:

Expand All @@ -145,8 +152,6 @@ To interact with your extended data, create a class to access it. You'll typical

#### Snippet Example

Here's how you might interact with your extended data in a Snippet:

``` php
$modx->addPackage('extendeduser', MODX_CORE_PATH . 'components/extendeduser/model/', 'ext_');
$user = $modx->getObject('extUser', 123); // where 123 is the id of a user
Expand All @@ -158,67 +163,27 @@ return print_r($data->toArray(), true);
#### More complex example

``` php
<?php
/**
* File sample.class.php (requires MODX Revolution 2.x)
* Created on Aug 18, 2010
* Project shawn_wilkerson
* @package extendedUser
* @version 1.0
* @category User Extension
* @author W. Shawn Wilkerson
* @link http://www.shawnWilkerson.com
* @copyright Copyright (c) 2010, W. Shawn Wilkerson. All rights reserved.
* @license GPL
*
*/
if (!class_exists('Extendeduser')) {
class Extendeduser
class Extendeduser
{
public function __construct(\MODX\Revolution\modX $modx, array $config = [])
{
$this->modx = $modx;
$extPath = $modx->getOption('core_path', null, MODX_CORE_PATH) . 'components/extendeduser/';
$this->modx->addPackage('extendeduser', $extPath . 'model/', 'ext_');
$this->userID = $modx->getOption('userID', $config, $modx->user->get('id'));
$this->userObj = $this->modx->getObject('modUser', $this->userID);
}

public function getPhoneObj()
{
function __construct(modX $modx, array $config= array ()) {
/* Import modx */
$this->modx= $modx;
/* Establish the environment */
$this->extPath= $modx->getOption('core_path',null, MODX_CORE_PATH).'components/extendeduser/';
$this->modx->addPackage('extendeduser', $this->extPath .'model/', 'ut_');
$this->_config= array_merge(array (
'userID' => $this->modx->user->get('id'),
), $config);
/* Define the user */
$this->userObj = $this->setUser($this->_config['userID']);
$this->userID = $this->userObj->get('id');
}
function __destruct() {
unset ($this->extPath, $this->userObj, $this->userID, $this->_config);
}
/**
* Returns object of type Phone.
*/
public function getPhoneObj() {
$this->userObj->getOne('Phones');
return $this->userObj->Phones;
}
/**
* Returns object utUser instance of modUser Defaults to current user.
* @param $userID
*/
public function getUserObj($userID) {
return $this->modx->getObject('modUser', $userID);
}
/**
* Establishes the user.
* @param int $userID
*/
public function setUser($userID){
return $this->getUserObj($userID);
}
return $this->userObj->getMany('Phones');
}
}
```

#### 5.) Accessing the extended user class
### 5. Access the extended user class

In our example we will be accessing our extended user throughout our site, therefore we load it as a service as shown in the following example:
Load the class as a service when the extended user is needed across the site:

``` php
<?php
Expand All @@ -232,14 +197,14 @@ return;

## Noteworthy items

1. Any pre-existing user will still have `modUser` as the `class_key` and therefore will not be extended or produce user objects of type `extUser` unless you change it.
1. Any pre-existing user still has `MODX\Revolution\modUser` as the `class_key`. It is not loaded as `extUser` until you change that value.
2. `modUser::setSudo()` – the preferred way to set the privileged `sudo` flag, since mass‑assignment of `sudo` is blocked in `modUser::set()`.
3. `modUser::save()` is overridden to fire MODX‑specific events (e.g. `OnUserBeforeSave`) and to set `createdon` for new users. Be aware that saving an extended user will trigger these events.
4. Double‑check the `modx.mysql.schema.xml` file to ensure you are not colliding with existing class names or aliases; otherwise your extensions may supersede the default `modUser` definitions (including attributes like `Profile`).
5. The `extUser` will **not** have a table created in the database, but the attached relations will.
6. The extended class table(s) **must** be in the same database as the regular `_modx_\_users` table.
7. Symptoms of step 3 (extension _packages path) not being correct:
- Any user with the `class_key` of `extUser` will return an error upon login: "User cannot be found…". If this is the admin, access your database directly, revert the `class_key` to `MODX\Revolution\modUser`, log in correctly, and then correct the path.
6. The extended class table(s) **must** be in the same database as `modx_users`.
7. Symptoms of a wrong model path:
- A user whose `class_key` is `extUser` cannot log in. The core does not print a dedicated sentence for that failure. If that user is the admin, set `class_key` back to `MODX\Revolution\modUser` in the database, log in, and then fix the path.
- The snippets attached to the class may intermittently work or fail altogether.
8. To get counts from your data (i.e. how many phone numbers does this person have) use either (any criteria can be added):

Expand All @@ -248,7 +213,7 @@ return;
$this->modx->getCount('extPhones');
```

Multiple extended modUser systems can run side by side: repeat the process for each extension, changing `class_key` to the respective extended class. They can even be combined — for example, by extending the rpx extension into a hybrid system.
Multiple extended modUser systems can run side by side: repeat the process for each extension, changing `class_key` to the respective extended class. They can even be combined, for example by extending the rpx extension into a hybrid system.

## Suggested additional considerations

Expand All @@ -261,11 +226,11 @@ Multiple extended modUser systems can run side by side: repeat the process for e

[modActiveDirectory](http://github.com/splittingred/modActiveDirectory) provides interaction with a Microsoft Domain Controller.

[rpx extension](http://github.com/opengeek/engaged) allows people to login via Facebook and other social networking mediums.
[rpx extension](http://github.com/opengeek/engaged) allows people to login via Facebook and other social networks.

## Modifying class\_key

Whenever you modify the class\_key for a built-in MODX object, you need to be aware of how behavior changes. The class\_key affects what aggregates and composites are available to the object. For example, if a user has a class\_key "extUser", you can still retrieve the object using the parent class:
Changing the class\_key of a built-in MODX object changes which aggregates and composites the object has. If a user has class\_key `extUser`, you can still retrieve the object with the parent class:

``` php
// both of these work when class_key is "extUser":
Expand Down
29 changes: 16 additions & 13 deletions en/extending-modx/developing-restful-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,12 @@ _old_id: "1728"
_old_uri: "2.x/developing-in-modx/advanced-development/developing-rest-servers"
---

MODX ships a small REST server stack built around `modRestService` and `modRestController`. You point HTTP requests at a bootstrap script, map URL paths to controller classes, and get CRUD behaviour over xPDO objects with hooks for auth, validation, and response shaping.
MODX ships a small REST server built on `modRestService` and `modRestController`. Point HTTP requests at a bootstrap script, map URL paths to controller classes, and you get CRUD over xPDO objects, with hooks for auth, validation, and the response shape.

This page covers MODX 2.x and MODX 3.x. Class locations changed in 3.x (`MODX\Revolution\Rest\*`). The public controller API matches across versions, the service config does not: 3.x adds `sanitize`, 2.x has `xmlDisableEntityLoader`.
This page covers MODX 2.x and MODX 3.x. In 3.x the classes live in `MODX\Revolution\Rest\*`. The public controller API matches across versions. The service config does not: 3.x adds `sanitize`, 2.x has `xmlDisableEntityLoader`.

> Outgoing HTTP from MODX to third-party APIs is a different stack. See [HTTP Client](extending-modx/services/http). The deprecated `modRest` client is documented under [Services](extending-modx/services/modrest).

## Recommended reading

Phil Sturgeon's *[Build APIs you won't hate](https://leanpub.com/build-apis-you-wont-hate)* is a solid primer on REST conventions before you wire controllers.

## In a nutshell

1. Create `rest/index.php` that boots MODX, configures `modRestService`, then calls `prepare()` and `process()`.
Expand Down Expand Up @@ -140,7 +136,7 @@ That 405 with the message `Method not allowed` means the router ran and found no

| Option | Default | Role |
| --- | --- | --- |
| `basePath` | site `base_path` | Directory of controller files |
| `basePath` | site `base_path` | Controller directory |
| `controllerClassPrefix` | `modRestController` | Prefix prepended to the class loaded from the file name |
| `controllerClassSeparator` | `''` | Joiner between prefix and class segment |
| `controllerClassFilePostfix` | `.php` | Extension used when scanning directories |
Expand All @@ -156,10 +152,10 @@ That 405 with the message `Method not allowed` means the router ran and found no
| `defaultSuccessStatusCode` / `defaultFailureStatusCode` | `200` / `200` | HTTP status codes used by `success()` / `failure()` |
| `responseSuccessKey` / `responseMessageKey` / `responseObjectKey` / `responseErrorsKey` | `success` / `message` / `object` / `errors` | Keys in a controller response body |
| `errorMessageSeparator` | `' '` | Separator between field errors |
| `trimParameters` | `false` | Trim whitespace from string request parameters |
| `trimParameters` | `false` | Does not trim the parsed body or query string. Trim strings in the controller |
| `sanitize` | `false` | 3.x only: run parameters through `modX::sanitize()` |
| `exitOnResponse` | `true` | End the script after sending the response |
| `xmlRootNode` | `response` | Root node of XML output |
| `xmlRootNode` | `response` | XML output root node |

An unknown suffix is not an error — the request falls back to `defaultResponseFormat`. Service-level error codes (`400`, `401`, `405`, `500`) always carry the keys `success`, `message`, `object`, and `code`; the `response*Key` options do not affect them.

Expand Down Expand Up @@ -237,9 +233,16 @@ Load the package with `addPackage()` (or your Extra's service) before `process()

The ID segment is copied onto `$this->primaryKeyField` (default `id`) before the method runs.

A controller without a method for the requested verb returns `405 Unsupported HTTP method`. A class that cannot be instantiated (an abstract base, for instance) returns `400`. If the verb method never calls `success()`, `failure()`, or `collection()`, the service answers with a second `405` instead of your payload.
A controller without a method for the requested verb returns status `405` and the message `Unsupported HTTP method get` (the verb is lowercase, at the end). A class that cannot be instantiated (an abstract base, for instance) returns `400`. If the verb method runs and never calls `success()`, `failure()`, or `collection()`, the service sends the controller response as it stands. It does not send a second `405`.

Parameters are collected per verb: `GET` reads the query string only, `POST` merges the query string with the body, `PUT` uses `$_POST` and the body (query string ignored), `DELETE` merges the query string with the body.
Parameters are collected per verb:

| Verb | Source |
| --- | --- |
| `GET` | Query string only |
| `POST` | Query string merged with the body |
| `PUT` | `$_POST` and the body. Query string ignored |
| `DELETE` | Query string merged with the body |

### List query parameters

Expand Down Expand Up @@ -290,7 +293,7 @@ Default `get` / `post` / `put` / `delete` already handle the happy path. Overrid
- `beforePut()`
- `beforeDelete()`

The object sits on `$this->object`. A `null` return means "continue" in `beforePost()` and `beforePut()`, but not in `beforeDelete()`: there `null` aborts the request and reaches `failure()` with an empty message. Return `true` explicitly.
The object sits on `$this->object`. In `beforePost()` and `beforePut()`, `null` and `true` both let the request continue. In `beforeDelete()`, only `true` continues. `null` there reaches `failure()` with an empty message. Return `true`.

Set the text the client sees on a `false` return with `setErrorMessage()`; add or drop individual field errors with `addFieldError()` / `removeFieldError()`.

Expand Down Expand Up @@ -387,7 +390,7 @@ The format comes from a path suffix or from `defaultResponseFormat`. Three ship

Request bodies are parsed by `Content-Type`: `application/json`, `application/xml`, or `application/x-www-form-urlencoded` (the default). With `image/jpeg`, `image/png`, or `image/gif` the properties receive an open `filehandle` stream.

When the service parses inbound XML request bodies, an empty XML element becomes an empty string (`''`) in the resulting PHP array, not an empty array. [#14305](https://github.com/modxcms/revolution/pull/14305) Controllers that treat "empty" with `empty()` or strict `=== ''` checks stay consistent with JSON clients sending `""`.
When the service parses inbound XML request bodies, an empty XML element becomes an empty string (`''`) in the PHP array. It does not become an empty array. [#14305](https://github.com/modxcms/revolution/pull/14305) Controllers that treat "empty" with `empty()` or strict `=== ''` checks stay consistent with JSON clients sending `""`.

`success()` and `failure()` bodies use the keys from the service config (`success`, `message`, `object`, plus `errors` when field errors exist) and always carry `code`. List calls use `results` and `total`.

Expand Down
Loading
Loading