CRUD: Creating, Reading, Updating and Deleting Objects

This tutorial assumes you are using Ariadne 9.0. Error handling and return values will work inconsistently in older Ariadne's. Specifically the return values are not always available. You can rely on the $this->error variable for errors, but they are usually just a string of text, in the language as set in $nls - if available. In Ariadne 9.0 all generated by the templates and functions below use ar::error() to generate an error object with a message and a numeric code.

Note: This tutorial skips the 'reading' part of the CRUD four, since this is already handled in other places.

1. CRUD Using Ariadne's system templates

Create a new object

<pinp>
    ar::call('system.new.phtml', array(
        'arNewType' => 'ppage',
        'arNewFilename' => 'filename',
        $nls => array(
            'name' => 'A new page'
        )
    ));
</pinp>

This requires the add grant on the parent object of the newly to create object. So you'll need something like add or add(ppage) or even =add(ppage). See the tutorial on grants for more information.

In the event of an error, the template will return false and set the variable $this->error. So with error checking this becomes:

<pinp>
    $newPath = ar::call('system.new.phtml', array(
        'arNewType' => 'ppage',
        'arNewFilename' => 'filename',
        $nls => array(
            'name' => 'A new page'
        )
    ));
    if ( !$newPath ) {
        echo $this->error;
    }
</pinp>

Errors have a localized message - if available, english otherwise - and a numeric error code. The default toString method will return these as one string. Not all templates will return errors in the format of ar::error(), so if you need to handle errors in a generic way, do this:

<pinp>
    ... some operation that may set $this->error
    if ( $this->error ) {
        if ( ar('error')->isError($this->error) ) {
            echo $this->error->getMessage();
        } else {
            // just a string
            echo $this->error;
        }
    }
</pinp>

Change the data of an existing object:

<pinp>
    $newPath = ar::get('filename')
    ->call('system.save.data.phtml', array(
        $nls => array(
            'name' => 'A new name'
        )
    ));
</pinp>

This requires the edit grant on the object. e.g. edit or edit(ppage). See the tutorial on grants for more information.

Important: If you are editing a pshortcut object, calling system.save.data.phtml will actually save the target of the shortcut instead of the shortcut itself. Instead you must call system.save.shortcut.phtml to update the shortcut itself.

Delete an object:

<pinp>
    $result = ar::get('filename')
    ->call('system.delete.phtml');
</pinp>

Deleting requires the delete grant, e.g. delete, >delete, delete(ppage), etc.

Deleting will fail if the object contains child objects. If you want to make sure you delete the whole tree, do this:

<pinp>
    ar::find('object.path ~="filename/%"')
    ->order('object.path DESC')
    ->call('system.delete.phtml');
</pinp>

This will find all child objects of 'filename/', including itself, and order them by path, longest path first. This means that the call to system.delete.phtml will be called on the child object before it is called on its parent. As long as each delete call succeeds, the calls to its parent later will also succeed.

Deleting a large number of objects can be time consuming, so you should consider doing this in smaller batches or in a seperate request. Or see section 3 on how to re-use Ariadne's own delete dialog.

General notes

When using the system templates to create or update an object, you can only specify variables that the system knows. These are the variables also in the edit and add dialogs. In addition you can specify any number of custom variables, using the customdata api. The advantage is that all objects will automatically set the correct properties when saved in this way. So you'll get the name.value, fulltext.value, references.value, etc.

Properties are how Ariadne can find objects fast. They are fast search index. Objects can contain any kind of data. This makes it difficult to optimize performance when searching for specific values. By defining a fixed set of properties with defined value types, Ariadne allows you to quickly find the correct objects.

2. CRUD using builtin functions

Calling the default system templates makes creating, updating and deleting object easy. But sometimes you need more control. You may want to add non-standard properties or non-standard variables outside the customdata api. In that case read on.

Creating a new object

<pinp>
    $ob = newobject('filename', 'ppage');
    $ob->setValue('name', 'A new page', $nls);
    $newPath = $ob->save();
</pinp>

The setValue() method makes sure the correct language object is made in the object's data. In this case it is setting this value ( assuming $nls is set to 'en' or english ):

$ob->data->en->name = 'A new page';

The difference with the system.new.phtml method is that calling save() directly skips setting the default properties for a specific type. You'll need to supply these yourself. The save() method only saves the properties time.ctime, time.mtime, time.muser and owner.value. In addition it will also save any custom properties specified in the custom fields dialog.

Luckily the save method allows you to add your own properties:

<pinp>
    $ob = newobject('filename', 'ppage');
    $ob->setValue('name', 'A new page', $nls);
    $newPath = $ob->save( array(
        'name' => array(
            array(
                'value' => 'A new page',
                'nls'   => $nls
            )
        )
    ));
</pinp>

Important: Any property data you pass on to save() will remove any previous values for that property. So to remove all previous data for the name property, simply do this:

<pinp>
    $ob = newobject('filename', 'ppage');
    $ob->setValue('name', 'A new page', $nls);
    $newPath = $ob->save( array(
        'name' => array()
    ));
</pinp>

The format to the properties argument is as follows:

$properties = array(
    '{$propertyName}' => array(
        {$index} => array(
            '{$propertyKey}' => {$propertyValue},
            ...
        )
    )
)

For every propertyName you add in the properties array, you must specify the complete set of data for that property. All previous data for that property is removed.

When saving a pshortcut object in this way, it is your responsibility to set the vtype value correctly. The vtype on a shortcut is set to the type of the target object. e.g.:

<pinp>
    $target = '/another/object/';
    $targetType = current( 
      ar::get($target)->call('system.get.type.phtml') 
    );
    $ob = newobject('filename', 'ppage');
    $ob->setValue('path', $target);
    $newPath = $ob->save(null, $targetType);
</pinp>

Creating new objects in this way is discouraged in practice, because you are circumventing all kinds of checks that happen in the system.new.phtml, or actually the system.save.data.phtml template of each type. You can create objects that don't have required data fields or that contain invalid data. The system.save.data.phtml is in fact the model for each object type.

Updating an existing object

<pinp>
    $ob = current( ar::get('filename')->call('system.get.phtml') );
    $ob->setValue('name', 'A new name', $nls);
    $newPath = $ob->save( array(
        'name' => array(
            array(
                'value' => 'A new page',
                'nls'   => $nls
            )
        )
    ));
</pinp>

Unlike creating objects using the newobject() call, there are many good reasons to use this approach for updating objects. Assuming the object has been created in a normal way, all it's data is valid. But you may need to enrich the data with something non-standard, e.g. workflow state. You could use the customdata api, but that limits how you store the property data. Instead do something like this:

<pinp>
    $state = ar::getvar('newState');
    $this->setValue('state', $state);
    $result = save( array(
        'state' => array(
            array(
                'value' => $newState
            )
        )
    ));
</pinp>

Note: You cannot use just any property name, Ariadne has a limited number of predefined properties, state is one of them.

Deleting an existing object

<pinp>
    $ob = current( ar::get('filename')->call('system.get.phtml') );
    $result = $ob->delete();
</pinp>

There are no advantages or disadvantages of using the delete() function instead of the system.delete.phtml template. Both do essentially the same.

Using Ariadne's dialogs

Ariadne provides complete dialogs with error handling for creating, updating and deleting objects. You can certainly use these in your own application. You'll need to use Ariadne's internal javascript framework to use these and the dialogs open in a popup window.

Adding an object

<pinp>
    $graphicsURL = ar::acquire('settings.graphicsURL');
</pinp>
<!doctype html>
<html>
<body>
    <button id="myAdd">Add</button>
    <script src="<pinp> 
        echo $graphicsURL; 
    </pinp>ariadne.load.js?muze+muze.event+muze.dialog"></script>
    <script>
        $('#myAdd').click( function() {
            muze.dialog.open('dialog.add.php', 'myAddDialog', {
                windowFeatures: 'width=400,height=600',
                createNewWindow: false
            })
            .on('submit', function(args) {
                // ... now browse to new path?
                var newURL = args['url'];
                window.location.href = newURL;
            })
            .always( function(action, args) {
                this.close();
            });
        });
    </script>
</body>
</html>

Note: The muze javascript framework has no dependencies on jQuery, it's just used here because it is familiar and more concise than either vanilla javascript or the muze.event javascript api.

I'm loading the muze javascript modules on the graphcisURL, since that is a good cacheable location. The ariadne.load.js template bundles the multiple modules into a single request.

The muze.dialog javascript api is used to communicate with popup dialogs. You call muze.dialog.open with a url, a window name and optionally some settings for the window. It returns a callback router in which you add callbacks for specific messages from the dialog.

The add dialog has one message it can send: 'submit'. It passes on the following arguments:

  • type
    The type of the new object.
  • name
    The name of the new object, not the filename!
  • path
    The fill path of the new object.
  • url
    The default URL of the new object, contains session and language information.

After the submit callback is called, the .allways() callback is called next. In this case it instructs the dialog to close itself. If you did not do this, the add dialog would stay open and allow the user to create another new object immediately. This is by design.

Editing an object

<!doctype html>
<html>
<body>
    <button id="myEdit">Edit</button>
    <script src="<pinp> echo $graphicsURL; </pinp>ariadne.load.js?muze+muze.event+muze.dialog"></script>
    <script>
        $('#myEdit').click( function() {
            muze.dialog.open('dialog.edit.php', 'myEditDialog', {
                windowFeatures: 'width=400,height=600',
                createNewWindow: false
            })
            .on('submit', function(args) {
                // reload this page
                window.location.reload(true);
            })
            .always( function(action, args) {
                this.close();
            });
        });
    </script>
</body>
</html>

The edit dialog works identical to the add dialog.

Deleting objects

<!doctype html>
<html>
<body>
    <button id="myDelete">Delete</button>
    <script src="<pinp> echo $graphicsURL; </pinp>ariadne.load.js?muze+muze.event+muze.dialog"></script>
    <script>
        $('#myDelete').click( function() {
            muze.dialog.open('dialog.delete.php', 'myDeleteDialog', {
                windowFeatures: 'width=400,height=600',
                createNewWindow: false
            })
            .on('submit', function(args) {
                // this page is deleted, browse to the parent?
                var showURL = args['showURL'];
                window.location.href = showURL;
            })
            .always( function(action, args) {
                this.close();
            });
        });
    </script>
</body>
</html>

The delete dialog also only has the submit message. But it calls it with these arguments:

  • childrenOnly
    Returns whether the 'delete children only' checkbox was checked.
  • showPath
    If the current object is deleted, this will contain the path to the existing parent object.
  • showURL
    Like showPath, but this contains the default URL for the parent. Only available from Ariadne 9.0 on.