In this tutorial, we will create the DocuVault file management system, which you can find fully coded in htdocs/tutorial
.
DocuVault is a very simple demo of a document management system designed to facilitate work between legal or auditing firms and their clients.
The company administrator can define new users and file types (contracts, deeds, invoices, etc.).
Clients can upload their files and categorize them. The company (administrators) can view files from all clients;
clients can only view their own files.
If you haven't already, follow the steps in the "Quick Start" guide to clone the repository and set up a development environment with Simplon PHP.
Make a copy of htdocs/blank
and rename it to htdocs/docuvault
.
In the lines:
5 Set the app name.
$AppName = 'docuvault';
38 'DEFAULT_ELEMENT' => 'AE_File',
39 'DEFAULT_METHOD' => 'showAdmin',
* If you are using Docker as indicated in the quick start guide, that's all.
In general, variables have self-explanatory names that you can modify according to your needs. We highlight the following lines:
33 The DB parameters.
8 and 9 The location of the Simplon base files.
15 The renderer.
21 The web address of the application.
29 The application language.
35 The time zone.
From the description of our app, we can see that we will have:
First, we will create the basic functionality and then add permissions, so we will create the classes corresponding to files and file types.
Let's start with something simple: the ability to upload files with a description. In htdocs/docuvault
, create AE_File.php
* with the following content.
*NOTE: Your application elements must have class names starting with AE_
for Application Element.
class AE_File extends SC_Element { static $ReturnBtnMsg, $CancelBtnMsg, $SearchBtnMsg, $SearchMsg, $CreateBtnMsg, $CreatedMsg, $CreateMsg, $CreateError, $UpdateBtnMsg, $UpdatedMsg, $UpdateMsg, $UpdateError, $DeleteBtnMsg, $DeletedMsg, $DeleteMsg, $DeleteError; function construct() { } }
Now we will add the DB key and the file and description fields in function construct()
.
function construct() { $this->id = new SD_AutoIncrementId(); $this->file = new SD_File('./files','File'); // 'Archivo' translated to 'File' $this->description = new SD_Text('Description'); // 'Descripción' translated to 'Description' }
Go to http://localhost/docuvault/
in your browser, you should see something like this:
(images obtained with the browser in dark mode)
This is the file management screen:
it consists of a search bar and a list of existing files. Click on the icon to add an element.
You should see the file creation form.
If you select a file, enter the description, and click create, you should see something like this:
To add fields to the search, we must modify the Element Data flags by adding an S
to the fields we want to be searchable.
class AE_File extends SC_Element { … .. . function construct() { $this->id = new SD_AutoIncrementId(); $this->file = new SD_File('./files','File','S'); // 'Archivo' translated to 'File' $this->description = new SD_Text('Description','S'); // 'Descripción' translated to 'Description' $this->type = new SD_ElementContainer(new AE_FileType(),'Type','S'); // 'Tipo' translated to 'Type' } }
Upon refreshing, we should see something like this:
This allows filtering the files in the list by filling in the fields and using the search button.
We now have a system that uploads files with their descriptions and allows filtering them.
Using the actions, you can also "view", edit, and delete them.
If you check the database at: http://localhost:8081/index.php
, you will see that a docuvault
database was created with an AE_File
table containing id
, description
, and file
columns, and it has one record (or more if you played around with the system).
This initial system is quite good, but still far from our requirements.
The system administrator must also be able to create different categories or file types (contracts, deeds, invoices, etc.).
Let's create the AE_FileType
element.
Create the file htdocs/docuvault/AE_FileType.php
<?php class AE_FileType extends SC_Element { static $ReturnBtnMsg, $CancelBtnMsg, $SearchBtnMsg, $SearchMsg, $CreateBtnMsg, $CreatedMsg, $CreateMsg, $CreateError, $UpdateBtnMsg, $UpdatedMsg, $UpdateMsg, $UpdateError, $DeleteBtnMsg, $DeletedMsg, $DeleteMsg, $DeleteError; function construct() { $this->id = new SD_AutoIncrementId(); $this->nombre = new SD_String('Name','SE'); // 'Nombre' translated to 'Name' } }
In your browser, open http://localhost/docuvault/AE_FileType/!showAdmin
And you will see the file type administrator. Create the types 'contract' and 'invoice'. You should have something like this:
File types are of little use if they are not used to categorize files. Let's add the file type to the files. In AE_File construct
add: $this->type = new SD_ElementContainer(new AE_FileType(),'Type');
It should look like this:
class AE_File extends SC_Element { … .. . function construct() { $this->id = new SD_AutoIncrementId(); $this->file = new SD_File('./files','File','S'); // 'Archivo' translated to 'File' $this->description = new SD_Text('Description','S'); // 'Descripción' translated to 'Description' $this->type = new SD_ElementContainer(new AE_FileType(),'Type'); // 'Tipo' translated to 'Type' } }
Back at http://localhost/docuvault/AE_File/!showAdmin
, you should see:
The Warning text appears only once and indicates that the structure of the AE_File
table has been updated.
When creating/uploading a new file, we will see a new field. The icon allows us to select an existing file type, while with
we can create a new file type.
We now have a system that allows uploading, categorizing, and listing files. Let's add the ability to view them.
For this, we will create a new version of SD_File.php
with different behavior when displaying the data, by rewriting the viewVal
method.
To do this, create the Datas
directory in the project folder and the file htdocs/docuvault/Datas/AD_File.php
* with the following content.
*NOTE: Your application data classes must have names starting with AD_
for Application Data, and be located in the Datas
subdirectory.
<?php class AD_File extends SD_File { public function viewVal(){ return new SI_Link($this->val(), $this->fileName()); } }
Additionally, we need to change the data type in our element. From SD_File
to AD_File
.
In the line $this->file = new SD_File('./files','File','S');
The S in SD_File
indicates to the framework that it is a Simplon file, while the A in AD_File
indicates it is specific to the application.
This system is of little use without user control and permissions;
to define these permissions, we will use the user and role classes.
We will start by modifying the last two lines of index.php
, they should say:
'PERMISSIONS' => 'AE_User', //'' 'LOAD_ROLE_CLASS' => true,
This tells Simplon that we want to use subclasses of the 'AE_User'
class, which in turn will inherit from SE_User
, to control access, and that these subclasses correspond to subclasses named the same as the roles.
Let's review SE_User->contruct
:
static::$cantAccessMsg = SC_Main::L('You can\'t access this page');
Defines the message users will see when trying to access a page for which they do not have permission.
self::$permissions = array( 'admin' => array('*'=>'allow'), 'user' => array( 'Admin'=>'deny', 'View'=>array( 'updateAction'=>'viewableWhen_id_=_CurrentUserId', // Typo corrected: 'viwableWhen' -> 'viewableWhen' 'createAction'=>'hide', 'deleteAction'=>'hide', ), 'Search'=>'deny', 'Update'=>array( 'id'=>'fixed_CurrentUserId', 'userRole'=>'fixed_CurrentUserRole', 'userName'=>'fixed_CurrentUserName', ), 'Create'=>'deny', 'Delete'=>'deny', '*'=>'deny', ), );
Defines which roles can view and modify users.
Users with the 'admin'
role can do everything array('*'=>'allow'),
.
Users with the 'user'
role cannot administer 'Admin'=>'deny'
, search 'Search'=>'deny'
, create 'Create'=>'deny'
, or delete 'Delete'=>'deny'
; they will not see links to create or delete.
Furthermore, they can only edit when the user ID to be edited matches their own ID, meaning they can only edit their own data.
'View'=>array( 'updateAction'=>'viewableWhen_id_=_CurrentUserId', // Typo corrected: 'viwableWhen' -> 'viewableWhen' 'createAction'=>'hide', 'deleteAction'=>'hide', ).
When editing, they cannot change their ID, role, or username.
'Update'=>array( 'id'=>'fixed_CurrentUserId', 'userRole'=>'fixed_CurrentUserRole', 'userName'=>'fixed_CurrentUserName', ) .
And anything else is forbidden.
'*'=>'deny' .
Finally, we see it has id
, username
, password
, full name
, role
, and an empty menu.
$this->id = new SD_AutoIncrementId(null);
$this->userName = new SD_String(SC_Main::L('User'),'VCUSlRe');
$this->password = new SD_Password(SC_Main::L('Password'));
$this->fullName = new SD_String(SC_Main::L('Full name'),'SL');
$role = new SE_Role();
$this->userRole = new SD_ElementContainer($role,SC_Main::L('Role'),null,'RSL');
$this->userRole->layout(new SI_Select());
$this->menu = new SI_SystemMenu([]);
We also see
$this->sourceURL = new SD_Hidden(null,'vcuslerf',$_SERVER['REQUEST_URI']);
Which is used to return to the page from which the login process originated and
$this->userRole->layout(new SI_Select());
which changes the role selection in forms to a dropdown menu.
On this basis, which already has methods for authentication and checking user and element permissions, we will build our permission system for the app with two roles: users AE_User.php
and administrators AE_Admin.php
.
We will also create the AE_EmptyAdmin
class, which automatically activates when there is no administrator user in the database.
htdocs/docuvault/AE_EmptyAdmin.php
<?php class AE_EmptyAdmin extends SE_EmptyAdmin { }
htdocs/docuvault/AE_User.php
<?php class AE_User extends SE_User { protected static $RoleID;
protected $defaultObject = 'AE_File', $defaultMethod = 'showAdmin';
static $menu, $ReturnBtnMsg, $CancelBtnMsg, $SearchBtnMsg, $SearchMsg, $CreateBtnMsg, $CreatedMsg, $CreateMsg, $CreateError, $UpdateBtnMsg, $UpdatedMsg, $UpdateMsg, $UpdateError, $DeleteBtnMsg, $DeletedMsg, $DeleteMsg, $DeleteError;
function construct($id = NULL, $storage = 'AE_User' ) { parent::construct($id, $storage);
//We need to ensure there is an user role in the DB and set it as a fixed value for this class if(!self::$RoleID){ $role = new SE_Role();
$role->roleName('user');
$search = new SE_Search(array('SE_Role')); $result = $search->getResults($role->toArray(), ['id'], 0, 1)[0]; if ($result && $result->id()) { self::$RoleID = $result->id();
} else { self::$RoleID = $role->create(); } }
@$Links[] = new SI_Link(SC_Main::$RENDERER->action('AE_File','showAdmin'), SC_MAIN::L('Files')); $Links[] = new SI_Link(SC_Main::$RENDERER->action($this,'showupdate'), SC_MAIN::L('My profile'));
$Links[] = new SI_AjaxLink(SC_Main::$RENDERER->action($this->getClass(),'logout'), SC_MAIN::L('Logout'), 'logout.svg');
self::$menu = new SI_SystemMenu($Links); } }
htdocs/docuvault/AE_Admin.php
<?php class AE_Admin extends AE_User { protected static $AdminRoleID;
static $menu, $ReturnBtnMsg, $CancelBtnMsg, $SearchBtnMsg, $SearchMsg, $CreateBtnMsg, $CreatedMsg, $CreateMsg, $CreateError, $UpdateBtnMsg, $UpdatedMsg, $UpdateMsg, $UpdateError, $DeleteBtnMsg, $DeletedMsg, $DeleteMsg, $DeleteError;
function construct($id = NULL, $storage = 'AE_User' ) { parent::construct($id, $storage); $this->storage('AE_User');
//We need to ensure there is an Admin role in the DB and set it as a fixed value for this class if(!self::$AdminRoleID){ $role = new SE_Role();
$role->roleName('admin');
$search = new SE_Search(array('SE_Role')); $result = $search->getResults($role->toArray(), ['id'], 0, 1)[0]; if ($result && $result->id()) { self::$AdminRoleID = $result->id();
} else { self::$AdminRoleID = $role->create(); } }
@$Links[] = new SI_Link(SC_Main::$RENDERER->action('AE_File','showAdmin'), SC_MAIN::L('Files')); $Links[] = new SI_Link(SC_Main::$RENDERER->action('AE_FileType','showAdmin'), SC_MAIN::L('File Types'));
// 'Files types' -> 'File Types'$Links[] = new SI_Link(SC_Main::$RENDERER->action('AE_User','showAdmin'), SC_MAIN::L('Users'));
$Links[] = new SI_Link(SC_Main::$RENDERER->action('SE_Role','showAdmin'), SC_MAIN::L('Roles')); $Links[] = new SI_AjaxLink(SC_Main::$RENDERER->action($this->getClass(),'logout'), SC_MAIN::L('Logout'), 'logout.svg');
self::$menu = new SI_SystemMenu($Links); } }
Let's analyze these files: EmptyAdmin
simply renames the existing Simplon class as an application class, which activates when no administrator is registered in the database.
AE_User
and AE_Admin
have:
$RoleID
variable to store the ID of the role type they will respectively have fixed.$defaultObject
and $defaultMethod
variables to define the default action for the user.parent::construct($id);
Call to the parent constructor.$this->storage = 'SE_User';
(Only AE_Admin
) so it uses the same table as AE_User
.if(!self::$RoleID){...}
code to ensure the corresponding role exists.@$Links[]... $this->menu = new SI_SystemMenu($Links);
the links and construction of the menu for users of that role.With these three files, upon re-entering http://localhost/docuvault/
, you should see the user creation screen and be able to create an administrator.
After creating it, log out and log in now with your new administrator.
You should see something like this:
And the link "Click here to go to your home page" will take you to the same screen.
If you use the top menu, you will see that you can access users and roles, but not Files or File Types.
This is because the initial screen AE_File->showAdmin
and neither the files nor the file types have defined permissions.
To define them, we need to define the $permissions
array like the one we saw in the SE_User
class.
Let's add this declaration to AE_File.php
:
static $permissions = array( 'admin' => array('*'=>'allow'), 'user' => array( ''=>'accessibleWhen_CurrentUserId_=_user', 'View'=>array( ''=>'accessibleWhen_CurrentUserId_=_user', 'user'=>'fixed_CurrentUserId', ), 'Admin'=>array( 'user'=>'fixed_CurrentUserId', 'updateAction'=>'viewableWhen_user_=_CurrentUserId', // Typo corrected: 'viwableWhen' -> 'viewableWhen' 'deleteAction'=>'hide', ), 'Search'=>array( ''=>'accessibleWhen_CurrentUserId_=_user', 'user'=>'fixed_CurrentUserId', ), 'Update'=>array( ''=>'accessibleWhen_CurrentUserId_=_user', 'user'=>'fixed_CurrentUserId', ), 'Create'=>array( 'user'=>'fixed_CurrentUserId', ), 'Delete'=>array( ''=>'accessibleWhen_CurrentUserId_=_user', 'user'=>'fixed_CurrentUserId', ), ), '*' => array('*'=>'deny') );
This tells the system:
That the administrator can do everything: 'admin' => array('*'=>'allow'),
The user can only view file information when the ID of the user who created the file matches (i.e., they can only view and manipulate their own files).
''=>'accessibleWhen_CurrentUserId_=_user',
''=>'accessibleWhen_CurrentUserId_=_user',
indicates that methods related to search ('Search'
), update ('Update'
), and delete ('Delete'
) are only accessible for the user's own files.
Creation doesn't need it because it doesn't have an ID yet.
'user'=>'fixed_CurrentUserId',
indicates that methods related to search ('Search'
), update ('Update'
), creation ('Create'
), and deletion ('Delete'
) must have the user ID value fixed to that of the active user.
If a new method is created, it can be included directly in the permissions as another key, for example:
'NewMethod'=>array( // 'NuevoMetodo' translated to 'NewMethod' ''=>'accessibleWhen_CurrentUserId_=_user', 'user'=>'fixed_CurrentUserId', ),
In addition to permissions, we need to add the user to our files.
In the constructor, we will add:
$this->user = new SD_ElementContainer(new SE_User(),'User',null,'SL');
// 'Usuario' translated to 'User'$this->user->layout(new SI_RadioButton());
And to change how the file type selection is presented, we will add:
$this->type->layout(new SI_Select());
In AE_FileType.php
the permissions are simpler:
static $permissions = array( 'admin' => array('*'=>'allow'), '*' => array('*'=>'deny') );
And indicate that only the administrator can view, create, modify, and delete file types.
And we will modify the assignment of the user
value by adding the user
method to AE_File
so that it modifies the path of file
.
This will allow files to be stored in a subdirectory corresponding to the user.
function user($val = null){ if($val){ $this->__call('user',[$val]); $userDir=sanitizeFileName($this->Ouser()->viewVal()); $this->file->path('./files/'.$userDir); //$this->downloadPath('./d/files/'.$userDir); }else{ return $this->__call('user',null); } }
Finally, we will make the administrator's initial screen one where they can choose, without going to the menu, between managing users, roles, files, and file types.
For this, we will change the initial method for administrator users to a custom screen.
To do this, in AE_Admin
we will set the initial class and method for administrators.
protected $defaultClass = 'AE_Admin', $defaultMethod = 'startScreen';
And we will create the corresponding method that will return a screen with four icons linking to the different tasks.
function startScreen(){ $content = new SI_VContainer(); $titleText = SC_Main::L('Welcome'); $title = new SI_Title($titleText,5,'c'); $content->addItem($title); $buttons = new SI_HContainer();
$buttons->addItem(new SI_Link(SC_Main::$RENDERER->action('AE_File','showAdmin'), new SI_Image('Files.svg','Files','500rem','500rem')));
$buttons->addItem(new SI_Link(SC_Main::$RENDERER->action('AE_FileType','showAdmin'), new SI_Image('FileTypes.svg','File Types','500rem','500rem'))); $buttons->addItem(new SI_Link(SC_Main::$RENDERER->action('AE_User','showAdmin'), new SI_Image('Users.svg','Users','500rem','500rem'))); $buttons->addItem(new SI_Link(SC_Main::$RENDERER->action('SE_Role','showAdmin'), new SI_Image('Roles.svg','Roles','500rem','500rem'))); $content->addItem($buttons);
$page = new SI_systemScreen( $content,$titleText );
return $page; }
This method uses various Simplon interface objects, whose classes are located in the \simplon-php\Renderers\htmlJQuery
directory and start with the SI_
prefix. Notable for their frequent use are: SI_VContainer
, SI_HContainer
, for arranging elements vertically and horizontally, and SI_systemScreen
for generating the system screen.
With this, we fulfill the project specifications:
“DocuVault is a very simple demo of a document management system to facilitate work between legal or auditing firms and their clients. The company administrator can define new users and file types (contracts, deeds, invoices, etc.). Clients can upload their files and categorize them. The company can view files from all clients; clients can only view their own files.”
However, there are still some things to refine, for example:
processUpdate
method for the AE_File
class.htaccess
access policy file in the Files
folder, which always executes a gatekeeper.php
file that checks the access policies for each file.