About CRUD Operations

What is PHP CRUD Generator?

PHP CRUD Generator is a program developed in pure PHP for building your complete Bootstrap administration panel using a visual UI.

PHPCG is suitable for both non-programmers and advanced PHP programmers, who will have access to a clean and well organized code so they can take advantage of all its potential.

PHPCG is able to analyze your database, extract tables, fields and any type of relationship intelligently.


The analyzed db data can then be used to generate your user administration panel:

Requirements

Database, Tables & Relations

PHPCG analyzes your database intelligently.

Your tables and fields must therefore respect the standards:

  • names in lower case letters, numbers and underscore (_)
  • consistent field types based on data
  • one-to-one, one-to-many and many-to-many relationships

Package Structure

Legend:

*
Required on your production server

Quick Start

About Local & Production workflow

The "generator" folder is only required to build & edit your Bootstrap admin panel.


  • If you use a local server + a remote server ("production server"):
    You can either:
    • use the local generator to create your admin panel then upload the "admin" and "conf" folders to your remote server.
    • create the admin panel directly from your production server.
    The 2nd way is the recommended method.
  • If you do not use a local server:
    the generator folder is required on your server.

Installation process Open the Tutorial

  1. 1Upload the required* folders on your server as described in the "package structure" section
  2. 2Open the installer - install/index.php - in your browser.
    If you use a local server + a remote server, you must run the installer on both.
    More informations available at the Installation/Registration section

    You'll have to enter your database connection settings for localhost or production server + basic general informations.
  3. 3 All is now ready to generate your admin panel using the CRUD Generator.
    Open the generator - generator/generator.php - in your browser.

Installation/Registration

If you use a local server + a remote server, you must run the installer on both.

What does the installer do?

The installer:

  • Tests your server compatibility (PHP Version, available modules, white rights, ...)
  • Tests & register your database connection credentials
  • Checks & registers your license
  • Creates a MySQL table named "user_data" with your license settings

If you need to reinstall, or have any question/issue with the installation/registration:

  1. Have a look at the video tutorials in the Tutorials
  2. Have a look at the "How To" & "Troubleshooting" sections in the Help Center
  3. Contact me if you still need help

Configuration

CORE configuration

Don't change anything here unless you know what you're doing.

USER configuration (General Settings)

This file contains some global settings that can be customized

To change these settings open the Generator in your browser and click the General Settings button.

generator_locked
Allows to lock/unlock the generator access.
If the generator is locked, access is protected by an identification page. You will need to enter your email & your purchase code to access it.
admin_locked
Protects the Bootstrap Admin Panel access with a login page if enabled.
The Authentication module must be installed.
This setting can be changed in the Generator page.
sitename
The main title displayed in the Bootstrap Admin Panel header
admin_logo
The image displayed at the top left of the Bootstrap Admin Panel header.
The image file MUST be in the admin/assets/images/ folder.
admin_action_buttons_position
Position of action buttons (update, delete) in the admin READ lists.
Can be ' left' or 'right'
auto_enable_filters
Enable filters in the admin lists as soon as a filter is selected in the drop-down list instead of clicking the filter button
lang
The Bootstrap Admin Panel language.
The translation file MUST exist in /admin/i18n/
locale_default
Date & time translations for the Bootstrap Admin lists
datetimepickers_style
Date & time pickers style (Default or Material Design)
datetimepickers_lang
Date & time pickers translations for the Bootstrap Admin forms.
The language files are located in:
  • class/phpformbuilder/plugins/
    pickadate/lib/compressed/translations/
    for default style (pickadate plugin)
  • class/phpformbuilder/plugins/material-datepicker/dist/i18n/ for Material Design style (material-datepicker plugin)
users_password_constraint
Security level of passwords required for creating user access.
The different configurations are available here: Passwords available patterns
collapse_inactive_sidebar_categories
Choose if the sidebar of the admin panel behaves like drop-down lists or accordion lists.

Admin Panel Skin (Admin Panel Bootstrap CSS)

Bootstrap CSS classes for the Admin Panel can be customized using the General Settings form

CRUD Generator

Protect access to the Generator with a login page

To protect access to the generator:

  1. Open the Generator in your browser
  2. Click the General Settings button to open the form
  3. Set Lock the Generator to true
  4. Done - when you open generator/generator.php in your browser you'll be redirected to the login page.
    Enter your registration email & your purchase code to login.

Main Panel

Choose your database in the dropdown list and validate. You will then see the complete panel appear.

PHP CRUD Generator Main screen

Read Lists

PHP CRUD Generator Read Lists

Lists filters

Add Filter

Click the "Add Filter" button in the Read Lists generator form to add a new filter.

This will add a new filter to your list.

PHPCG offers you 2 types of filters:

Simple filters

You just have to choose the field to filter in the dropdown list.

Advanced filters

Advanced filters are useful:

  • if you want to display 2 or more values in the admin dropdown list.
    For example the first and last name are displayed and the filtered value is the ID.
  • if you want to filter values from external relationships.

To use advanced filters it is necessary to enter query parameters with joins.

A help button is available to help you build your query, as well as a preview button that allows you to view the generated drop-down list and check its validity.

To build your requests we recommend the excellent software FlySpeed SQL Query


Create/Update Forms

Always build the list view first, and then the forms.

PHPCG Create/Update Forms

Delete Forms

Always build the list view first, and then the forms.

PHPCG Delete Forms

Admin User Authentification Module

The User Authentification Module installer allows you to configure the rights access to the admin items.
It should therefore be installed last, after having created all the elements of the CRUD.

Click the Install User Authentification Module button to launch the installer.

The installer will create the users and the users_profiles tables

You will also have enter the main administrator informations.

The main administrator can then manage users and profiles in the admin panel.

phpcg authentification module installer

Reinstall / Update the Admin User Authentification Module

If you add some tables to your admin panel after having installed the User Authentification Module you'll have to update the users_profiles table.

For that purpose you have two possibilities:

reinstall with the automatic installer

update with a simple SQL query

When you install / reinstall the authentication module, the users_profiles table used by PHP CRUD Generator to manage user rights is modified. For each table of your database used in the admin panel, 4 fields are created:

Instead of using the automatic installer, you can add the 4 fields with a simple MySQL query.
The result will be exactly the same as using the installer.

Copy / paste the query below into your database management interface (ie: phpmyadmin) to add the 4 fields to the users_profiles table.

Replace users_profiles with your users_profiles table name, which may have a prefix.

Replace _table with your the name of the table that you want to add to the authentication module.

ALTER TABLE `users_profiles`
ADD `read_table` BOOLEAN NOT NULL DEFAULT TRUE AFTER `profile_name`,
ADD `update_table` BOOLEAN NOT NULL DEFAULT TRUE AFTER `read_table`,
ADD `create_delete_table` BOOLEAN NOT NULL DEFAULT TRUE AFTER `update_table`,
ADD `constraint_query_table` VARCHAR(255) NULL DEFAULT NULL AFTER `create_delete_table`;

Bootstrap Admin Panel

Access, protection and login

As long as you have not installed and activated the authentication module (login), the admin panel access is public.

To enter the admin panel, open /admin/home

If you encounter a 404 error, you'll find the cause and solution in the Help center

If you try to login at /admin/login it'll logically fail, because the authentication module is not yet installed.

The authentication module must be installed at the end of the process, when you have built all your READ lists & forms with the generator.

Structure

Main navigation

The navbar is organized into categories and elements.

Each element represents a table in the database and gives access to the page of the MySQL data table (READ List).

Users can create, edit, delete categories, organize them by drag-and-drop, and organize elements in the same way.

The interface also allows you to choose an icon for each element.

Access to the navbar management interface is via the CRUD Generator, button "Organize Navbar".

Organize the Bootstrap 4 Dashboard Navbar

The information is recorded in a simple JSON file: php-crud-generator/admin/crud-data/nav-data.json

This file can be edited manually, it is not necessary to go through the interface.

Bootstrap 4 Dashboard Code samples

The coders will probably appreciate to have a preview of the codes of the admin panel files generated by PHPCG

Here are some code samples:

Table READ List - PHP Object

This is the main PHP Class which gets the records from the database and build all the values

<?php
namespace crud;

use common\Utils;
use phpformbuilder\database\Mysql;
use phpformbuilder\database\Pagination;
use secure\Secure;

class Actor extends Elements
{

    // item name passed in url
    public $item;

    // item name dispolayed
    public $item_label;

    // associative array : field => field displayed name
    public $fields;

    // external relations
    public $external_tables_count = 1;
    public $external_fields_count;
    public $external_rows_count;
    public $external_fields = array();

    // primary key passed to create|edit|delete
    public $primary_key; // primary key fieldname
    public $primary_key_alias; // primary key alias for query

    // CREATE rights
    public $can_create = false;

    // authorized primary keys for restricted updates
    public $authorized_update_pk = array();

    public $pk = array(); // primary key values for each row
    public $actor_id = array();
    public $first_name = array();
    public $last_name = array();
    public $last_update = array();

    public $export_data_button;
    public $filtered_columns = '';
    public $filters_form;
    public $join_query       = '';
    public $pagination_html;
    public $records_count;
    public $select_number_per_page;
    public $sorting;
    public $url;

    public function __construct($element)
    {
        $this->table             = $element->table;
        $this->item              = $element->item;
        $this->item_label        = $element->item_label;
        $this->primary_key       = $element->primary_key;
        $this->primary_key_alias = $element->primary_key;
        $this->select_data       = $element->select_data;
        $this->fields            = $element->fields;

        $table = $this->table;

        if (file_exists(ADMIN_DIR . 'crud-data/' . $this->item . '-filter-data.json')) {
            $json = file_get_contents(ADMIN_DIR . 'crud-data/' . $this->item . '-filter-data.json');
            $filters_array = json_decode($json, true);
        }
        $this->url        = $_SERVER['REQUEST_URI'];
        $qry_start        = 'SELECT `actor`.`actor_id`, `actor`.`first_name`, `actor`.`last_name`, `actor`.`last_update` FROM actor';

        // restricted rights query
        $qry_restriction = '';
        if (Secure::canReadRestricted($table)) {
            $qry_restriction = Secure::getRestrictionQuery($table);
        }

        // filters
        $filters           = new ElementsFilters($table, $filters_array, $this->join_query);
        $filters_where_qry = $filters->returnWhereQry();
        if (!empty($qry_restriction)) {
            $filters_where_qry = str_replace('WHERE', 'AND', $filters_where_qry);
        }

        if (isset($_SESSION['filtered_columns']) && is_array($_SESSION['filtered_columns'])) {
            $this->filtered_columns = implode(', ', $_SESSION['filtered_columns']);
        }
        $this->filters_form = $filters->returnForm($this->url);

        $active_filters_join_queries = array();

        // Get join queries from active filters
        $active_filters_join_queries = $filters->buildElementJoinQuery();

        // sorting query
        $this->sorting = ElementsUtilities::getSorting($table, $this->primary_key, 'ASC');
        $qry_sorting   = " ORDER BY" . $this->sorting;

        $db = new Pagination();
        $pagination_url = $_SERVER['REQUEST_URI'];
        if (isset($_POST['npp']) && is_numeric($_POST['npp'])) {
            $_SESSION['npp'] = $_POST['npp'];
        } elseif (!isset($_SESSION['npp'])) {
            $_SESSION['npp'] = 20;
        }

        // echo $qry_start . $qry_restriction . $active_filters_join_queries . $filters_where_qry . $qry_sorting;
        $this->pagination_html = $db->pagine($qry_start . $qry_restriction . $active_filters_join_queries . $filters_where_qry . $qry_sorting, $_SESSION['npp'], 'p', $pagination_url, 5, true, '/', '');

        $this->records_count = $db->rowCount();
        $primary_key = $this->primary_key_alias;
        if (!empty($this->records_count)) {
            while (!$db->endOfSeek()) {
                $row = $db->row();
                $this->pk[] = $row->$primary_key;
                $this->actor_id[]= $row->actor_id;
                $this->first_name[]= $row->first_name;
                $this->last_name[]= $row->last_name;
                $this->last_update[]= $row->last_update;
            }
        }

        // CREATE/DELETE rights
        if (Secure::canCreate($table) || Secure::canCreateRestricted($table) === true) {
            $this->can_create = true;
        }

        // restricted UPDATE rights
        if (Secure::canUpdateRestricted($table) === true) {
            $transition = 'WHERE';
            if (!empty($filters_where_qry)) {
                $transition = 'AND';
            }

            // get authorized update primary keys
            $qry_start .= Secure::getRestrictionQuery($table, $transition);
            $db = new Pagination();
            $pagination_html = $db->pagine($qry_start . $filters_where_qry . $qry_sorting, $_SESSION['npp'], 'p', $pagination_url, 5, true, '/', '');
            $records_count = $db->rowCount();
            $primary_key = $this->primary_key_alias;
            if (!empty($records_count)) {
                while (!$db->endOfSeek()) {
                    $row = $db->row();
                    $this->authorized_update_pk[] = $row->$primary_key;
                }
            }
        } elseif (Secure::canUpdate($table) === true || Secure::canUpdateRestricted($table) === true) {
            // user can update ALL records
            $this->authorized_update_pk = $this->pk;
        }

        /* external relations */

        for ($i=0; $i < count($this->pk); $i++) {
            $this->external_rows_count[$i] = array();
            $this->external_fields[$i] = array();
            $db = new Mysql();
                    // actor => film_actor => film
                $qry = 'SELECT film.film_id, film.title, film.description, film.release_year FROM actor INNER JOIN `film_actor`
            ON `film_actor`.`actor_id`=`actor`.`actor_id` INNER JOIN `film`
            ON `film_actor`.`film_id`=`film`.`film_id` WHERE actor.' . $this->primary_key . ' = ' . $this->pk[$i];
            $db->query($qry);
            $this->external_rows_count[$i][] = $db->rowCount();
            $ext_fields = array(
                'table'       => 'film',
                'table_label' => 'film',
                'uniqid'      => 'f-' . uniqid(),
                'fields'      => array(
                        'title' => array(),
                        'description' => array(),
                        'release_year' => array()
                    )
            );
            if (!empty($this->external_rows_count[$i])) {
                while (! $db->endOfSeek()) {
                    $row = $db->row();
                    $ext_fields['fields']['title'][]  = $row->title;
                    $ext_fields['fields']['description'][]  = $row->description;
                    $ext_fields['fields']['release_year'][]  = $row->release_year;
                    $ext_fields['fields']['action'][]  = '<div class="btn-group"><a href="film_actor/edit/' . $row->film_id . '" class="btn btn-xs btn-warning legitRipple" title="View" data-popup="tooltip" data-delay="500"><span class="fas fa-pencil-alt"></span></a><a href="film_actor/delete/' . $row->film_id . '" class="btn btn-xs btn-danger legitRipple" title="Delete" data-popup="tooltip" data-delay="500"><span class="fas fa-times-circle"></span></a></div>';
                } // end while
            } // end if
            $this->external_fields[$i][] = $ext_fields;
        } // end for
        $this->external_fields_count = count($this->external_fields);

        // Export data button
        $this->export_data_button = ElementsUtilities::exportDataButtons($table, 'SELECT * FROM ' . $table . ' ORDER BY ' . $this->primary_key . ' ASC', 'excel, csv');

        // number/page
        $numbers_array = array(5, 10, 20, 50, 100, 200, 10000);
        $this->select_number_per_page = ElementsUtilities::selectNumberPerPage($numbers_array, $_SESSION['npp'], $this->url);
    }
}
                    

Table READ List - TWIG Template

Once the PHP Object has been built, the view is built with the help of a clean TWIG template:

    <div class="card {{ constant('DEFAULT_CARD_CLASS') }} mr-4">
        <div class="card-header d-lg-flex flex-wrap justify-content-between {{ constant('DEFAULT_CARD_HEADING_CLASS') }}">
            {% if object.records_count > 0 %}

            <div class="d-flex ml-auto order-lg-2">
                {{ object.select_number_per_page|raw }}
            </div>

            <hr class="w-100 d-lg-none">

            {% endif %}
            <div class="d-flex order-lg-0 mb-3 mb-sm-0">
                {% if object.can_create == true %}
                <a href="{{ constant('ADMIN_URL') }}{{ object.item }}/create" class="btn btn-sm mr-1 btn-primary d-flex align-items-center legitRipple"><i class="{{ constant('ICON_PLUS') }} position-left"></i>{{ constant('ADD_NEW') }}</a>
                {% endif %}
                {% if object.records_count > 0 %}
                {{ object.export_data_button|raw }}
                {% endif %}
            </div>

            <div class="order-lg-1 mx-lg-auto">
                <form name="rp-search-form" id="rp-search-form" action="" class="form-inline justify-content-center">
                    <div class="form-group">
                        <div class="input-group">
                            <div id="rp-search-field" class="dropdown input-group-prepend">
                                <a class="dropdown-toggle pl-4 pr-3 rounded-left border-left border-top border-bottom" id="search-dropdown-link" data-toggle="dropdown" aria-haspopup="true"
                                        aria-expanded="false"></a>
                                <div class="dropdown-menu" aria-labelledby="search-dropdown-link">
                                    {% for field_name, field_display_name in object.fields %}
                                    {% set active = '' %}
                                    {% if field_name == attribute(session.rp_search_field, object.table) %}
                                    {% set active = ' active' %}
                                    {% endif %}
                                    <a class="dropdown-item{{ active }}" href="#" data-value="{{ field_name }}">{{ field_display_name }}</a>
                                    {% endfor %}
                                </div>
                            </div>
                            {% set search_value = '' %}
                            {% if attribute(session.rp_search_string, object.table) is defined %}
                            {% set search_value = attribute(session.rp_search_string, object.table) %}
                            {% endif %}
                            <input id="rp-search" name="rp-search" type="text" value="{{ search_value }}" placeholder="{{ constant('SEARCH') }}" class="form-control flex-grow-1">
                            <div class="input-group-append">
                                <button id="rp-search-submit" class="btn btn-secondary ladda-button" data-style="zoom-in" type="submit"><span class="ladda-label"><i class="{{ constant('ICON_SEARCH') }}"></i></span></button>
                            </div>
                        </div>
                    </div>
                </form>
            </div>

        </div>

        {# Partial block list - rendered alone on the research results #}
        {% block object_list %}

        <div id="{{ object.item }}-list">

        {% if object.records_count > 0 %}

            <div class="table-responsive">
                <table class="table table-striped table-condensed table-data">
                    <thead>
                        <tr class="{{ constant('DEFAULT_TABLE_HEADING_BACKGROUND') }}">
                            {% if constant('ADMIN_ACTION_BUTTONS_POSITION') == 'left' %}
                            <th>{{ constant('ACTION_CONST') }}</th>
                            {% endif %}

                            <th class="sorting">{{ object.fields.actor_id }}<a href="#" class="sorting-up" data-field="actor_id" data-direction="ASC"><i class="{{ constant('ICON_ARROW_UP') }}"></i></a><a href="#" class="sorting-down" data-field="actor_id" data-direction="DESC"><i class="{{ constant('ICON_ARROW_DOWN') }}"></i></a></th>
                            <th class="sorting">{{ object.fields.first_name }}<a href="#" class="sorting-up" data-field="first_name" data-direction="ASC"><i class="{{ constant('ICON_ARROW_UP') }}"></i></a><a href="#" class="sorting-down" data-field="first_name" data-direction="DESC"><i class="{{ constant('ICON_ARROW_DOWN') }}"></i></a></th>
                            <th class="sorting">{{ object.fields.last_name }}<a href="#" class="sorting-up" data-field="last_name" data-direction="ASC"><i class="{{ constant('ICON_ARROW_UP') }}"></i></a><a href="#" class="sorting-down" data-field="last_name" data-direction="DESC"><i class="{{ constant('ICON_ARROW_DOWN') }}"></i></a></th>
                            <th class="sorting">{{ object.fields.last_update }}<a href="#" class="sorting-up" data-field="last_update" data-direction="ASC"><i class="{{ constant('ICON_ARROW_UP') }}"></i></a><a href="#" class="sorting-down" data-field="last_update" data-direction="DESC"><i class="{{ constant('ICON_ARROW_DOWN') }}"></i></a></th>
                            <th>film</th>
                            <th>{{ constant('DISPLAY') }}</th>
                        {% if constant('ADMIN_ACTION_BUTTONS_POSITION') == 'right' %}
                            <th>{{ constant('ACTION_CONST') }}</th>
                        {% endif %}
                        </tr>
                    </thead>
                    <tbody>
                    {% for i in range(0, object.records_count - 1) %}
                        <tr>
                            {% if constant('ADMIN_ACTION_BUTTONS_POSITION') == 'left' %}
                            <td class="has-btn-group no-ellipsis">
                                <div class="btn-group">
                                    {% if object.pk[loop.index0] in object.authorized_update_pk %}
                                    <a href="{{ constant('ADMIN_URL') }}{{ object.item }}/edit/{{ object.pk[loop.index0] }}" class="btn btn-sm btn-warning legitRipple" data-tooltip="{{ constant('EDIT') }}" data-delay="500"><span class="{{ constant('ICON_EDIT') }} icon-md"></span></a>
                                    {% endif %}
                                    {% if object.can_create == true %}
                                    <a href="{{ constant('ADMIN_URL') }}{{ object.item }}/delete/{{ object.pk[loop.index0] }}" class="btn btn-sm btn-danger legitRipple" data-tooltip="{{ constant('DELETE_CONST') }}" data-delay="500"><span class="{{ constant('ICON_DELETE') }} icon-md"></span></a>
                                    {% endif %}
                                </div>
                            </td>
                            {% endif %}
                        <td>{{ object.actor_id[ loop.index0 ] }}</td>
                        <td>{{ object.first_name[ loop.index0 ] }}</td>
                        <td>
                        {% if object.pk[loop.index0] in object.authorized_update_pk %}
                           <span class="jedit-text tip" data-field="last_name" data-delay="500" title="{{ constant('CLICK_TO_EDIT') }}" id="actor-last_name-actor_id-{{ object.pk[ loop.index0 ] }}">{{ object.last_name[ loop.index0 ] }}</span>
                        {% else %}
                            {{ object.last_name[ loop.index0 ] }}
                        {% endif %}
                </td>
                        <td>{{ toDate(object.last_update[ loop.index0 ], 'dd MMMM yyyy H:m a')|raw }}</td>
                            {% if object.external_tables_count > 0 %}
                            {% for j in range(0, object.external_tables_count - 1) %}
                            <td class="no-ellipsis">
                                {% if object.external_rows_count[i][j] > 0 %}
                                <h6 class="card-title text-center text-nowrap mb-2"><span class="badge bg-gray-300 position-left">{{ object.external_rows_count[i][j] }}</span><a class="dropdown-toggle" data-toggle="collapse" href="#{{ object.external_fields[i][j]['uniqid'] }}" role="button" aria-expanded="false" aria-controls="{{ object.external_fields[i][j]['uniqid'] }}"><small class="text-muted nowrap">{{ constant('SHOW') }} / {{ constant('HIDE') }}</small></a></h6>
                                <div class="collapse" id="{{ object.external_fields[i][j]['uniqid'] }}">
                                {{ object.external_add_btn[i][j]|raw }}
                                    <table class="table table-striped table-condensed">
                                        <thead class=" {{ constant('DEFAULT_TABLE_HEADING_BACKGROUND') }}">
                                            <tr>
                                                {% for field, value in object.external_fields[i][j].fieldnames %}
                                                <th>{{ value }}</th>
                                                {% endfor %}
                                            </tr>
                                        </thead>
                                        <tbody>

                                            {# Loop records #}

                                            {% for k in range(0, object.external_rows_count[i][j] - 1) %}
                                            <tr>

                                                {# Loop fields #}

                                                {% for field, value in object.external_fields[i][j].fields %}
                                                <td>{{ object.external_fields[i][j].fields[field][k]|raw }}</td>
                                                {% endfor %}
                                            </tr>
                                            {% endfor %}
                                        </tbody>
                                    </table>
                                </div>
                                {% else %}
                                {{ object.external_add_btn[i][j]|raw }}
                                {% endif %}
                            </td>
                            {% endfor %}
                            {% endif %}
                            <td><a href="{{ constant('BASE_URL') }}" data-delay="500" data-tooltip="{{ constant('OPEN_URL') }}" target="_blank"><span class="{{ constant('ICON_NEW_TAB') }} text-center"></span></a></td>
                            {% if constant('ADMIN_ACTION_BUTTONS_POSITION') == 'right' %}
                            <td class="has-btn-group no-ellipsis">
                                <div class="btn-group">
                                    {% if object.pk[loop.index0] in object.authorized_update_pk %}
                                    <a href="{{ constant('ADMIN_URL') }}{{ object.item }}/edit/{{ object.pk[loop.index0] }}" class="btn btn-sm btn-warning legitRipple" data-tooltip="{{ constant('EDIT') }}" data-delay="500"><span class="{{ constant('ICON_EDIT') }} icon-md"></span></a>
                                    {% endif %}
                                    {% if object.can_create == true %}
                                    <a href="{{ constant('ADMIN_URL') }}{{ object.item }}/delete/{{ object.pk[loop.index0] }}" class="btn btn-sm btn-danger legitRipple" data-tooltip="{{ constant('DELETE_CONST') }}" data-delay="500"><span class="{{ constant('ICON_DELETE') }} icon-md"></span></a>
                                {% endif %}
                                </div>
                            </td>
                            {% endif %}
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div> <!-- END table-responsive -->

            {% else %}
            <div class="card-body">
                <p class="text-semibold">
                    {{ alert(constant('NO_RECORD_FOUND'), 'alert-info has-icon')|raw }}
                </p>
            </div>
            {% endif %}

            <div class="card-footer  {{ constant('DEFAULT_CARD_FOOTER_CLASS') }} p-4 mt-5">
                {{ object.pagination_html|raw }}
            </div>
        </div> <!-- END {{ object.item }}-list -->

        {% endblock object_list %}
        {# END Partial block - rendered alone on the research results #}

    </div> <!-- END card -->

                    

Table Update form - PHP Form

The Form generated to edit records from the given table.

The form is built with PHP Form Builder

All the operations are made in the same file:

  • Protected by the users authentication & rights management module
  • Get records to prefill the form
  • Create and show the form, including all the plugins (select dropdowns, pickers, uploaders, ...)
  • PHP Validation
  • Update the database records or show the errors if wrong values are posted
<?php
use phpformbuilder\Form;
use phpformbuilder\Validator\Validator;
use phpformbuilder\database\Mysql;
use common\Utils;
use secure\Secure;

include_once ADMIN_DIR . 'secure/class/secure/Secure.php';

/* =============================================
    validation if posted
============================================= */

if ($_SERVER["REQUEST_METHOD"] == "POST" && Form::testToken('form-edit-actor') === true) {
    include_once CLASS_DIR . 'phpformbuilder/Validator/Validator.php';
    include_once CLASS_DIR . 'phpformbuilder/Validator/Exception.php';
    $validator = new Validator($_POST);
    $validator->required()->validate('actor_id');
    $validator->integer()->validate('actor_id');
    $validator->min(0)->validate('actor_id');
    $validator->max(65535)->validate('actor_id');
    $validator->required()->validate('first_name');
    $validator->maxLength(45)->validate('first_name');
    $validator->required()->validate('last_name');
    $validator->maxLength(45)->validate('last_name');
    $validator->required()->validate('last_update');
    $validator->date()->validate('last_update');

    // check for errors
    if ($validator->hasErrors()) {
        $_SESSION['errors']['form-edit-actor'] = $validator->getAllErrors();
    } else {
        require_once CLASS_DIR . 'phpformbuilder/database/db-connect.php';
        require_once CLASS_DIR . 'phpformbuilder/database/Mysql.php';
        $db = new Mysql();
        $update['first_name'] = Mysql::SQLValue($_POST['first_name'], Mysql::SQLVALUE_TEXT);
        $update['last_name'] = Mysql::SQLValue($_POST['last_name'], Mysql::SQLVALUE_TEXT);
        $update['last_update'] = Mysql::SQLValue($_POST['last_update'] . ' ' . $_POST['last_update-time'], Mysql::SQLVALUE_DATETIME);
        $filter["actor_id"] = Mysql::SQLValue($_POST['actor_id']);
        $db->throwExceptions = true;
        try {
                // begin transaction
                $db->transactionBegin();

                // update actor
            if (DEMO !== true && !$db->updateRows('actor', $update, $filter)) {
                $error = $db->error();
                $db->transactionRollback();
                throw new \Exception($error);
            } else {
                // ALL OK
                $db->transactionEnd();
                $_SESSION['msg'] = Utils::alert(UPDATE_SUCCESS_MESSAGE, 'alert-success has-icon');

                // reset form values
                Form::clear('form-edit-actor');

                // redirect to list page
                $page_link = '';
                if (isset($_SESSION['previous_page_number'])) {
                    $page_link = '/p' . $_SESSION['previous_page_number'];
                }
                header('Location:/admin/actor' . $page_link);

                // if we don't exit here, $_SESSION['msg'] will be unset
                exit();
            }
        } catch (\Exception $e) {
            $msg_content = DB_ERROR;
            if (ENVIRONMENT == 'development') {
                $msg_content .= '<br>' . $e->getMessage() . '<br>' . $db->getLastSql();
            }
            $_SESSION['msg'] = Utils::alert($msg_content, 'alert-danger has-icon');
        }
    } // END else
} // END if POST
$actor_id = $pk;
if (!isset($_SESSION['errors']['form-edit-actor']) || empty($_SESSION['errors']['form-edit-actor'])) { // If no error registered
    $qry = "SELECT * FROM `actor`";

    $transition = 'WHERE';

    // if restricted rights
    if (Secure::canUpdateRestricted('actor')) {
        $qry .= Secure::getRestrictionQuery('actor');
        $transition = 'AND';
    }
    $qry .= ' ' . $transition . " actor.actor_id = '$actor_id'";

    $db = new Mysql();
    $db->query($qry);
    if ($db->rowCount() < 1) {
        if (DEBUG === true) {
            exit($db->getLastSql() . ' : No Record Found');
        } else {
            Secure::logout();
        }
    }
    $row = $db->row();
    $_SESSION['form-edit-actor']['actor_id'] = $row->actor_id;
    $_SESSION['form-edit-actor']['first_name'] = $row->first_name;
    $_SESSION['form-edit-actor']['last_name'] = $row->last_name;
    $_SESSION['form-edit-actor']['last_update'] = $row->last_update;
}
$form = new Form('form-edit-actor', 'horizontal', 'novalidate', 'bs4');
$form->setAction('/admin/actor/edit/' . $actor_id);
$form->startFieldset();
$form->setCols(2, 10);
$form->addInput('hidden', 'actor_id', '');
$form->groupInputs('first_name', 'last_name');
$form->setCols(2, 4);
$form->addInput('text', 'first_name', '', 'First Name', 'required');
$form->addInput('text', 'last_name', '', 'Last Name', 'required');
$form->groupInputs('last_update', 'last_update-time');
$form->setCols(2, 10);
$form->addPlugin('pickadate', '#last_update', 'custom-date', array('%format%' => 'dd mmmm yyyy')); // date field
$form->addPlugin('pickadate', '#last_update-time', 'custom-time', array('%interval%' => 15, '%format%' => 'H:i a')); // time field

$form->setCols(2, 6);
$form->addInput('text', 'last_update', '', 'Last Update', 'required');
$form->setCols(0, 4);
$form->addInput('text', 'last_update-time', '', '', 'required, placeholder=Heure');
$form->setCols(2, 10);
$form->addBtn('button', 'cancel', 0, '<i class="' . ICON_BACK . ' position-left"></i>' . CANCEL, 'class=btn btn-warning legitRipple, onclick=history.go(-1)', 'btn-group');
$form->addBtn('submit', 'submit-btn', 1, SUBMIT . '<i class="' . ICON_CHECKMARK . ' position-right"></i>', 'class=btn btn-success legitRipple', 'btn-group');
$form->setCols(0, 12);
$form->centerButtons(true);
$form->printBtnGroup('btn-group');
$form->endFieldset();
$form->addPlugin('nice-check', 'form', 'default', array('%skin%' => 'green'));
                    

Customization for advanced users

If your database structure changes along the way, PHPCG is able to rebuild the data and allows you to regenerate the corresponding CRUD pages.

When generating administration panel pages, PHPCRUD automatically keeps a backup of the previous version.

The file comparison tool integrated with the generator allows you to compare side by side your current version and the previous one, and to merge them by choosing the code blocks to be retained.

Administration customizations can thus be retained during version/structure changes.

Update instructions

Updates are automatic.

When a new version is released, you'll see the "New PHP CRUD GENERATOR version is available" message in /generator/generator.php and will just have to click the "Install" button.

Your version number is available in /conf/conf.php (VERSION)


Special update from version 1.0 (first release)

  1. Copy /generator/update folder to /generator/update on your server.
  2. Open generator/update/first-update.php in your browser.
    This will install /vendor folder on your server and replace /generator/generator.php with the new version included in this package.
  3. Open /generator/generator.php in your browser and click the Install button.
  4. The automatic installer will start make the update

Languages/Translation (I18n)

PHP CRUD Generator and the generated Bootstrap admin panel are both fully multi-language.

To translate to your own language:

  1. Duplicate admin/i18n/en.php and rename it to your own language.
  2. Make the translations inside the file that you created (admin/i18n/[your-language].php)
  3. Open conf/user-conf.php and replace define('LANG', 'en'); with the filename you used previously.
  4. Check class/phpformbuilder/plugins/select2/dist/js/i18n/[your-language].js and create it if it doesn't exist.
  5. You're welcome to send us your translation, it'll be useful for other users.

PHP Form Builder

PHP Form Builder is included in the package and you can use it without restriction on the same domain as your CRUD.

This means that you can build any form you want on your website/project and use the integrated plugins & functionalities.

To use PHP Form Builder in your project, include the main conf file on your page add this code at the beginning of your file:

use phpformbuilder\Form;

session_start();
include_once include_once rtrim($_SERVER['DOCUMENT_ROOT'], DIRECTORY_SEPARATOR) . 'conf/conf.php';
include_once CLASS_DIR . 'phpformbuilder/Form.php';

Then you can build your forms. Documentation is available on the official website: https://www.phpformbuilder.pro

Drag & drop Form Builder & Templates

The drag & drop Form Builder & the templates are not included. This is not a restriction, but simply because they're available online, and most users don't need it, thus reducing the weight of the PHPCG package.

If you want to obtain these files just ask, please contact us.

Sources & Credits

Many thanks to the authors for their great work

Changelog

After any update, close & reopen your browser to clear PHP SESSION

version 1.15 (12/2020)


New Features:
    - add a new 'html' field type in the generator to show the HTML content in the admin lists instead of HTML code when the fields contain HTML
    - auto enable textarea + tinyMce in the generator for HTML fields
Improvements:
    - add $mail->Sender in Form.php for PHPMailer to improve email deliverability
    - edit the Fileuploader PHP image upload script to crop the images AFTER resizing
    - (the original behaviour that center-crops the original image is still available in the file code comments)
    - add a default empty value in the admin forms select, radio & checkbox when the field is not required
Bug Fix:
    - validator now validates integers with leading zeros (PHP :: Bug #43372)
    - fix wrong ajax POST url in the admin search whith paginated results
    - fix textarea custom heights in CREATE forms

version 1.14 (11/2020)


New Features:
    - add field height option for textarea in the generator
Improvements:
    - Accept NULL date / time instead of registering the default '1970-01-01 00:00' timestamp in database
    - sanitize directory separator in class/Form.php to avoid wrong plugins url detection on server with inconsistent $_SERVER['SCRIPT_NAME'] and $_SERVER['SCRIPT_FILENAME'] values
    - show tinymce and word char count in the generator only for textarea
Bug Fix:
    - remove php warning when posting a delete form without choosing yes/no

version 1.13.3 (11/2020)


Improvements:
    - improve scrolling behavior in admin nested tables show / hide
Bug Fix:
    - fix non-working nested tables show / hide due to the new OverlayScrollbars plugin

version 1.13.2 (11/2020)


Bug Fix:
    - replace the deprecated admin table scrollbar plugin broken by the latest jQuery with the great new OverlayScrollbars plugin

version 1.13.1 (11/2020)


New Features:
    - new tutorial to customize the admin Home page: https://www.phpcrudgenerator.com/tutorials/how-to-customize-the-bootstrap-admin-homepage
    - add documentation to update the Authentication Module with a simple SQL query instead of reinstalling from scratch:
      https://www.phpcrudgenerator.com/documentation/index#admin-user-authentification-module
Bug Fix:
    - fix a bug in the General Settings Form due to the previous update

version 1.13 (11/2020)


Bug Fix:
    - update jQuery to 3.5.1 due to a recent browser bug that prevented the generator forms to submit (nothing happened after clicking the submit button)

version 1.12 (11/2020)


Bug Fix:
    - prefill the generator create/update form properly with TinyMce and character counter options & values
    - remove php warning when installing the authentication module
    - move the generator scripts to the  part to avoid jQuery not loaded error in some special circumstances
    - change the target table in READ lists nested tables EDIT buttons to the end relationnal table instead of intermediate
    - edit class/Utils/isValidTimeStamp function to return true with number entry as well as string

version 1.11 (09/2020)


Improvements:
    - trim $url in CrudTwigExtension::ifRemoteFileExists($url) - vendor/twig/twig/src/Extension/CrudTwigExtension.php
    - replace "url" property in object classes with "item_url" to avoid conflicts with database fields named "url"
Bug Fix:
    - fix php Notice when building single record READ lists
    - add empty default value in create  / update forms for fields that get their values from a table when no record exist
    - fix the filtered columns overlay colored by colorColumns in the admin READ lists
    - fix wrong default dates / times in UPDATE forms with the pickadate & material date/time pickers hidden fields
    - fix date value with date pickers when a form is posted with errors
    - fix the index of the colored columns in READ lists when some filters are active with bulk delete enabled and admin action buttons are on the left

version 1.10 (06/2020)


Improvements:
    - add "open url button" link to the documentation in the generator
    - better date & time formats management with the material datepicker plugin
      (rebuild your create/update forms if you want to benefit from these changes)
Bug Fix:
    - fix date and time custom formats with translations in the create / edit forms
    - add missing session_start() in ajax bulk delete forms

version 1.9 (06/2020)


New Features:
    - add Bulk Delete capabilities to the admin dashboard's data lists
    - add date range picker filter to the generator filters options + the admin dashboard's data lists
    - add "Default field for search" option to the generator
Improvements:
    - improve the generator design consistency
    - improve root path detection for servers with inconsistent directory separators
    - collapse admin inactive sidebar categories on categorie click
Bug Fix:
    - fix Ajax filter results when the result options use 2 field names

version 1.8 (05/2020)


Improvements:
    - add timezone to the generator general settings
    - update PHP Form Builder to the latest version (4.4)
    - upgrade Twig to Twig 3.0 and others vendor libraries for PHP 7.4.x compatibility
    - add php DOM extension test in the installer's server capabilities tests
    - add a clear error message with a link to the help center on root path detection failure
    - add a loading indicator to the auto-updater
Bug Fix:
    - fix wrong urls in admin forms when moving the admin files from localhost to the production server
    - fix the missing relational values in the exported data
    - fix admin login failure after reinstalling the authentication module with changing the user table name
    - replace the double quotes with single quotes in the generator delete form template main query

version 1.7.7 (04/2020)


Bug Fix:
    - fix stupid ROOT path error with subfolder installations due to the previous update

version 1.7.6 (04/2020)


Improvements:
    - add a server test file in the install folder to debug paths & urls
    - auto-apply ORDER BY changes from the generator to the admin panel without clearing PHP session
    - update ElementFilters to allow simple quotes in advanced filters
Bug Fix:
    - fix ROOT path with server alias

version 1.7.5 (04/2020)


New Features:
    - add an "Ajax loading" option in the generator READ Lists filters (default: false)
        Hint: Enable Ajax loading on all the tables that contain a lot of records
        This new option allows to load the filters options on demand and will GREATLY improve the loading speed
    - add ORDER BY in the generator READ List main settings
    - add website search to https://www.phpcrudgenerator.com documentation, tutorials & help center
    - add default skin loader for each Bootstrap admin theme CSS in the general settings form
    - add the item name in the admin header h1
    - add a footer template for admin READ lists (admin/templates/footer.html)
Improvements:
    - cleaner generator design
    - add instructions to solve 404 errors on some servers (lightspeed) in the help center + admin/.htaccess
    - various minor optimizations
Bug Fix:
    - fix Tinymce's Responsive file manager url
    - edit the cUrl test file in install/

version 1.7.4 (12/2019)


New Features:
    - new setting available to choose to show search results in all on the same page or in a paginated list
      IMPORTANT: regenerate your READ lists from the generator if you want the paginated search results
                 or your paginated results will lead to 404 NOT FOUND
Bug Fix:
    - fix nested table records in READ lists with only the primary key displayed
    - fix the "add new" button link (previously to 404) in READ lists nested tables with page > 1
    - the generator delete form now sets the correct stored options for external tables records

version 1.7.3 (12/2019)


New Features:
    - add an "Advanced" section in the tutorials with a new "Date and Time formats management logic tutorial
Bug Fix:
    - fix wrong date / time formats in admin READ lists for servers without PHP intl extension in some random cases depending on the chosen format
    - fix date / time format dropdown helpers in the generator

version 1.7.2 (11/2019)


Improvements:
    - add Czech translation
    - improve documentation
Bug Fix:
    - fix PHP warnings with forms & array values
    - fix error in general settings form when no logo is registered
    - fix filters query with number values & MySQL 5.7+
    - fix error in the Italian translation
    - fix PHP warning caused by primary keys aliases in the admin READ lists external relations

version 1.7.1 (08/2019)


Bug Fix:
    - Fix the Admin Dropdown Search field cross-browser compatibility
      (rebuild your lists to apply)

version 1.7 (08/2019)


New Features:
    - New live search with Ajax Autocomplete for Bootstrap Admin Panel READ lists
      (rebuild your lists to apply)
Improvements:
    - Update Material Pickers for compatibility

version 1.6.1 (07/2019)


New Features:
    - Admin filters now can deal with JSON array values (select multiple, checkboxes)
    - New PHP CRUD Generator Tutorials channel on Youtube
Improvements:
    - Array values from database now displayed as comma-separated values instead of raw JSON
    - improve the online Documentation & Tutorials
Bug Fix:
    - Rewrite code to limit users rights to their own records
      (rebuild your lists / forms to apply)

version 1.6 (06/2019)


New Features:
    - 20+ new Bootstrap themes are now available
    - Choose your preferred Bootstrap theme from the General Settings form
    - Customize all the main layout Bootstrap CSS classes from the General Settings form
    - Compile the SASS files with Gulp using the new PHP CRUD Generator Gulp Github repository
    - New tutorial for Admin Theming & CSS: https://www.phpcrudgenerator.com/tutorials/how-to-customize-the-bootstrap-admin-panel-css
Bug Fix:
    - Great, no known bug!

version 1.5.6 (06/2019)


Improvements:
    - The General Settings form in the generator now allows to change the Bootstrap admin main body class
Bug Fix:
    - the installer was broken by the previous changes. Solved now.
    - Edit in place is no more available in Admin READ lists for users with insufficient rights
    - the broken "enable/disable" authentication module in the Generator now works again

version 1.5.5 (06/2019)


New Features:
    - The date & Time pickers languages can now be defined in the General Settings form
    - You can now choose the style of the Bootstrap admin date & Time pickers
      (default | Material Design)
    - New Italian translation - Many thanks to Alberto

version 1.5.4 (06/2019)


New Features:
    - New General Settings form available in the Generator
    - The action buttons of the Bootstrap Admin panel can now be on the left or right of the table
    - The filters of the Bootstrap Admin panel can now be triggered automatically when selected
    - You can change the site title and admin logo using the General Settings form
    - You can change the admin language using the General Settings form
    - You can change the admin skin using the General Settings form
Improvements:
    - Show custom table names in Admin READ lists nested tables
Bug Fix:
    - The Validation button in the Generator should now never overlap the forms

version 1.5.3 (06/2019)


New Features:
    - Action buttons in the admin panel can now be displayed in the
      1st column of the admin READ lists
Improvements:
    - Responsive & others in admin CSS
Bug Fix:
    - datepicker plugin
    - files & images upload
    - tooltips
      (these bugs were due to the previous update with latest PHP Form Builder)

version 1.5.2 (05/2019)


Bug Fix:
    - fix sorting buttons in admin panel READ lists

version 1.5.1 (05/2019)


Bug Fix:
    - fix export to excel/csv in admin panel

version 1.5 (05/2019)


New Features:
    - replace PHP Form Builder with the latest version 4.2.1
Improvements:
    - Admin Panel Fast Loading optimization with the new LoadJS features
    - PHP CRUD Fast Loading optimization with the new LoadJS features
    - rewrite queries for admin restricted users rights
    - upgrade Bootstrap to the latest version 4.3.1
    - minor various others improvements

version 1.4.9 (05/2019)


New Features:
    - add new Export features (print - current view - all records) in admin READ lists

version 1.4.8 (02/2019)


New Features:
Improvements:
    - add (very) strong protection for fileuploader plugin uploads
Bug Fix:
    - remove some PHP warnings
    - solved admin sidebar duplicate items issue

version 1.4.7 (02/2019)


Bug Fix:
    - fix navbar issue with empty icons

version 1.4.6 (02/2019)


New Features:
    - New "array" field type in generator for checkboxes & select multiple values
      will show JSON decoded values in the READ lists
Improvements:
    - better admin navbar content management ("Organize Navbar")
    - improve array values management in the generator
Bug Fix:
    - fix non-working select multiple with "set" & "enum" field types
    - fix changelog url in auto-update success message

version 1.4.5 (02/2019)


New Features:
    - License system now accepts domain with multiple extensions
      ie: domain.com, domain.eu, domain.co.uk are all valid with the same license.
    - New button in the Generator to reload fresh database structure
      (When you add or remove tables)
Improvements:
Bug Fix:
    - admin filters now accept zero values
    - fix queries in admin lists on external tables with direct relation (no intermediate table)

version 1.4.4 (02/2019)


New Features:
    - external records from relational tables can now be managed
      from the READ LISTS & the CREATE/UPDATE forms (!)
    - add self-referential foreign keys management
    - tables can now be removed/re-enabled from the admin navbar
    - add Spanish admin translation (Thanks to Sergio)
Improvements:
    - export buttons (csv/xls[x]) now export the exact filtered list items
    - align single fields on the left in admin panels

Bug Fix:
    - remove phone validation in auth. module installer
    - logout from generator/generator.php now does its job as intended
    - upgrade PHPMailer to latest 6.0.6 to fix PHP 7.3 warnings

version 1.4.3 (12/2018)


Bug Fix:
    - fix inverted label & value in form CREATE/EDIT templates
    - protect relation tables SELECT queries in form CREATE/EDIT templates

version 1.4.2 (10/2018)


New Features:
    - new "Add New" button in admin READ lists on external nested tables even if no record
Improvements:
    - ADMIN panel: register URL query parameters in $_GET (Altorouter ROUTES doesn't deal with these).
    - the ADMIN ADD & UPDATE forms now redirect to the correct list if we come from a nested table (external relation)
    - move date_default_timezone_set from conf/conf.php to conf/user-conf.php
    - add Help & instructions for Microsoft IIS & NGINX servers
Bug Fix:
    - "Add New" button in admin READ lists now always targets the right CREATE form
       even if there's several external nested tables in the list.
    - Fix several warnings & minor issues

version 1.4.1 (10/2018)


New Features:
    - add "Add New", "Edit" & "Delete" buttons in READ Lists nested tables for external tables records
Improvements:
    - add compatibility for date & time without PHP intl extension
Bug Fix:
    - definitely fix the Apache mod_security error on the install process with some misconfigured servers

version 1.4 (10/2018)

Warning: If you admin READ lists have date or datetime fields, open the corresponding templates in /admin/templates, find the functions toDate(...) and replace the date PHP format with the corresponding ICU date format.

new online PHP CRUD Tutorials


New Features:
    - PHPCG includes now the complete latest PHP Form Builder version with all its features & plugins.
    - Add the online knowledge base with numerous tutorials & videos
Improvements:
    - improve date & time translations management - https://www.phpcrudgenerator.com/tutorials/how-to-translate-dates-times-in-admin-panel
    - add full date & time translation in admin lists & forms
    - change admin form action from absolute url to root relative url
    - add install/curl-test.php to help with CURL debbuging
Bug Fix:
    - the generator now retrieves the correct stored values to be displayed in READ lists for the external fields
    - get the correct time value in admin edit forms with datetime fields
    - solve plugins URL detection with paths containing uppercase letters

version 1.3.2 (08/2018)


Improvements:
    - dates edit in place now get the current field value
    - image now crop from the center
Bug Fix:
    - fix missing fields in update forms due to previous update error
    - fix admin lists bug with fields having uppercase characters
    - fix admin edit in place with dates & uppercase table name

version 1.3.1 (08/2018)


Improvements:
Bug Fix:
    - fix Generator form create profiles

version 1.3 (08/2018)


Notes:
        - After this update you may have to reinstall the user authentification module from the Generator page.
Improvements:
    - update server-side validation functions to accept empty values,
            except for the validators whose internal logic make values required.
            Details available here: https://www.phpformbuilder.pro/documentation/class-doc.html#php-validation-methods
    - the User Authentification Module now keeps the users & users profiles tables and records when uninstalling.
    - the User Authentification Module can now be reinstalled even if the users & users profiles table exist
    - improve user profiles management and rights limitations
    - the users rights changes now take effect without clearing the session
    - the admin sidebar doesn't show empty categories anymore
    - the only required fields in users table are now name, firstname, profile ID, email, pass & active
      (takes effect on new User Authentification Module installs only)
    - add simulate property to Generator.php to simulate when we reset a table structure from generator
    - remove several warnings & improve various feedback messages
Bug Fix:
    - solve problem with updates & SSL errors on misconfigured servers

version 1.2.4 (07/2018)


Notes:
        - After this update you may have to reinstall the user authentification module from the Generator page.
Improvements:
        - set default empty value for passwords in UPDATE FORMS
Bug Fix:
        - solve CREATE/UPDATE forms generation with custom validation
        - solve READ LISTS generation with advanced filders
        - solve image path in admin when the field thumbs are not enabled
        - remove password validation in UPDATE FORMS if posted value is empty
        - correct select values count in generator CREATE/UPDATE forms with custom values
        - solve error 500 when adding new users

version 1.2.3 (07/2018)


New Features:
        - add an uninstallation process
        - add a login module for the generator on the production server
        - primary key management in admin forms
Improvements:
        - remove the "select database" form in generator & auto select the correct database
        - add warnings for non-standard tables & field names (hyphenated)
        - improve password fields management in CREATE/UPDATE forms:
          better password encryption with Secure class
          password are now automatically optional on update forms with an helper text: "Leave blank to keep the current password"
        - turn fileuploader debug on for CREATE/UPDATE forms
        - improve documentation
        - improve auto-validation detection according to forms & database field types
Bug Fix:
        - revert Twig template engine to version 1.35.4 to preserve PHP < 7.0 compatibility
        - regenerate css & js combined plugin files for CREATE/UPDATE forms when the forms are edited with the generator
        - fix generator which failed to validate when custom validators were selected while generating the CREATE/UPDATE forms
        - fix password encryption when changes are made in CREATE/UPDATE users table

version 1.2.2 (07/2018)


Improvements:
        - add user-conf file to avoid breaking user custom settings with updates
        - move the install folder outside the generator folder.
        - improve the updater script
        - improve url & path management
Bug Fix:
        - fix server issues in some special configurations

version 1.2.1 (07/2018)

Warning: if your authentification module is not enabled, after the update open php-crud-generator/conf/admin-lock.php and set ADMIN_LOCKED to false.


Improvements:
        - move ADMIN_LOCKED and ADMIN_LOGO to separate files for easier updates
Bug Fix:
        - fix several minor bugs

version 1.2 (06/2018)


Bug Fix:
        - fix authentification module installation (wrong users filters)

version 1.1 (06/2018)


New Features:
        - add File uploader to Generator + Admin panel
        - add version check & auto-updater
Improvements:
        - update dependencies & move to vendor with Composer
        - improve ROOT path analysis
Bug Fix:
        - correct date & time validation
        - correct value/display inversion with live-edit custom select

version 1.0 (06/2018)


First Release