Compare commits
4 commits
6c74b2a719
...
07b6fa1305
Author | SHA1 | Date | |
---|---|---|---|
|
07b6fa1305 | ||
|
2f38884798 | ||
|
f9829d4aea | ||
|
347de8eeaf |
27 changed files with 1702 additions and 535 deletions
|
@ -22,7 +22,8 @@
|
|||
}
|
||||
],
|
||||
"require": {
|
||||
"brenard/eesyphp": "@dev"
|
||||
"brenard/eesyphp": "@dev",
|
||||
"brenard/php-unidecode": "dev-master"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
|
|
|
@ -6,6 +6,7 @@ use EesyPHP\Date;
|
|||
use EesyPHP\Log;
|
||||
|
||||
use EesyPHPExample\Db;
|
||||
use EesyPHPExample\Db\Item;
|
||||
|
||||
use function EesyPHP\___;
|
||||
|
||||
|
@ -23,15 +24,15 @@ if (php_sapi_name() != "cli")
|
|||
**/
|
||||
|
||||
function print_item_info($item) {
|
||||
printf(_("Item #%s:\n"), $item['id']);
|
||||
printf("\t"._("ID: %s")."\n", $item['id']);
|
||||
printf("\t"._("Name: '%s'")."\n", $item['name']);
|
||||
printf("\t"._("Date: %s")."\n", Date :: format($item['date']));
|
||||
printf(_("Item #%s:\n"), $item->id);
|
||||
printf("\t"._("ID: %s")."\n", $item->id);
|
||||
printf("\t"._("Name: '%s'")."\n", $item->name);
|
||||
printf("\t"._("Date: %s")."\n", Date :: format($item->date));
|
||||
printf(
|
||||
"\t"._("Description: %s")."\n",
|
||||
($item['description']?"'".$item['description']."'":_("Not set"))
|
||||
($item->description?"'".$item->description."'":_("Not set"))
|
||||
);
|
||||
printf("\t"._("Status: %s")."\n", $item['status']);
|
||||
printf("\t"._("Status: %s")."\n", $item->status);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -40,11 +41,11 @@ function print_item_info($item) {
|
|||
* Common CLI commands
|
||||
**/
|
||||
|
||||
$orderbys = array('id', 'name', 'date', 'status', 'description');
|
||||
function cli_list($command_args) {
|
||||
global $orderbys;
|
||||
$params = array(
|
||||
'order' => $orderbys[0],
|
||||
'filters' => [],
|
||||
'order' => Item :: default_order(),
|
||||
'order_direction' => 'ASC',
|
||||
'all' => true,
|
||||
);
|
||||
|
@ -54,7 +55,7 @@ function cli_list($command_args) {
|
|||
case '-o':
|
||||
case '--orderby':
|
||||
$i++;
|
||||
if(!in_array($command_args[$i], $orderbys))
|
||||
if(!in_array($command_args[$i], Item :: possible_orders()))
|
||||
Cli :: usage('Invalid --orderby clause');
|
||||
$params['order'] = $command_args[$i];
|
||||
break;
|
||||
|
@ -67,7 +68,7 @@ function cli_list($command_args) {
|
|||
$i++;
|
||||
if(!check_status($command_args[$i]))
|
||||
Cli :: usage('Invalid -s/--status clause');
|
||||
$params['status'] = $command_args[$i];
|
||||
$params['filters']['status'] = $command_args[$i];
|
||||
break;
|
||||
default:
|
||||
$patterns[] = $command_args[$i];
|
||||
|
@ -77,7 +78,7 @@ function cli_list($command_args) {
|
|||
if (!empty($patterns))
|
||||
$params['pattern'] = implode(' ', $patterns);
|
||||
|
||||
$items = Db :: search_items($params);
|
||||
$items = Item :: search($params);
|
||||
if (!is_array($items)) {
|
||||
Log :: error("Invalid DB info return.\n");
|
||||
return False;
|
||||
|
@ -101,11 +102,11 @@ function cli_list($command_args) {
|
|||
foreach($items['items'] as $info) {
|
||||
$tbl->addRow(
|
||||
array(
|
||||
$info['id'],
|
||||
$info['name'],
|
||||
Date :: format($info['date']),
|
||||
$info['status'],
|
||||
($info['description']?$info['description']:''),
|
||||
$info->id,
|
||||
$info->name,
|
||||
Date :: format($info->date),
|
||||
$info->status,
|
||||
($info->description?$info->description:''),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -120,7 +121,7 @@ Cli :: add_command(
|
|||
___("[patterns]"),
|
||||
array(
|
||||
___("-o|--orderby Ordering list criterion. Possible values:"),
|
||||
" - ".implode("\n - ", $orderbys),
|
||||
" - ".implode("\n - ", Item :: possible_orders()),
|
||||
___("-r|--reverse Reverse order"),
|
||||
___("-s|--status Filter on status. Possible values:"),
|
||||
" - ".implode("\n - ", array_keys($status_list)),
|
||||
|
@ -132,7 +133,7 @@ function cli_show($command_args) {
|
|||
Cli :: usage(_('You must provide a valid ID.'));
|
||||
|
||||
$item_id = $command_args[0];
|
||||
$item = Db :: get_item($item_id);
|
||||
$item = Item :: get($item_id);
|
||||
|
||||
if (!$item)
|
||||
Log :: fatal(_("Item #%s not found."), $item_id);
|
||||
|
@ -157,7 +158,7 @@ function cli_delete($command_args) {
|
|||
|
||||
// Check exist
|
||||
$item_id = $command_args[0];
|
||||
$item = Db :: get_item($item_id);
|
||||
$item = Item :: get($item_id);
|
||||
if (!$item)
|
||||
Log :: fatal(_("Item #%s not found."), $item_id);
|
||||
|
||||
|
@ -173,7 +174,7 @@ function cli_delete($command_args) {
|
|||
}
|
||||
echo "\n";
|
||||
|
||||
if (!Db :: delete_item($item['id']))
|
||||
if (!$item -> delete())
|
||||
Log :: fatal(_("An error occurred deleting item #%d."), $item_id);
|
||||
|
||||
return True;
|
||||
|
@ -187,7 +188,7 @@ Cli :: add_command(
|
|||
|
||||
function cli_export($command_args) {
|
||||
$fd = fopen((count($command_args) >= 1?$command_args[0]:'php://output'), 'w');
|
||||
Db :: export_items($fd);
|
||||
Item :: export($fd);
|
||||
fclose($fd);
|
||||
Log :: info("Items export to '".(count($command_args) >= 1?$command_args[0]:'STDOUT')."'.");
|
||||
}
|
||||
|
@ -200,7 +201,7 @@ Cli :: add_command(
|
|||
|
||||
function cli_restore($command_args) {
|
||||
$fd = fopen((count($command_args) >= 1?$command_args[0]:'php://stdin'), 'r');
|
||||
Db :: restore_items($fd);
|
||||
Item :: restore($fd);
|
||||
fclose($fd);
|
||||
Log :: info(
|
||||
"Items restored from '%s'",
|
||||
|
@ -243,33 +244,33 @@ function cli_cron($command_args) {
|
|||
'Invalid $item_max_age value set in configuration: it\'s must be a positive integer.');
|
||||
Log :: debug("cli_cron(): item max age = $item_max_age day(s)");
|
||||
|
||||
$limit = time() - ($item_max_age * 86400);
|
||||
$limit = Date :: from_timestamp(time() - ($item_max_age * 86400));
|
||||
Log :: debug("Handle items expiration with creation date limit ".Date :: format($limit).".");
|
||||
|
||||
$items = Db :: search_items(array('all' => true));
|
||||
$items = Item :: search(array('all' => true));
|
||||
$error = false;
|
||||
foreach($items['items'] as $item) {
|
||||
if ($item['date'] < $limit) {
|
||||
if ($item -> date < $limit) {
|
||||
if ($just_try) {
|
||||
Log :: debug('Just-try mode: do not really delete item #%s (%s, creation date: %s)',
|
||||
$item['id'], $item['name'], Date :: format($item['date'])
|
||||
$item->id, $item->name, Date :: format($item->date)
|
||||
);
|
||||
}
|
||||
else if (Db :: delete_item($item['id'])) {
|
||||
else if ($item->delete()) {
|
||||
Log :: info('Item #%s (%s) deleted (creation date: %s)',
|
||||
$item['id'], $item['name'], Date :: format($item['date'])
|
||||
$item->id, $item->name, Date :: format($item->date)
|
||||
);
|
||||
}
|
||||
else {
|
||||
Log :: error('Fail to delete item "%s" (%s, creation date: %s)',
|
||||
$item['id'], $item['name'], Date :: format($item['date'])
|
||||
$item->id, $item->name, Date :: format($item->date)
|
||||
);
|
||||
$error = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log :: debug('Item "%s" (%s) still valid (creation date: %s)',
|
||||
$item['id'], $item['name'], Date :: format($item['date'])
|
||||
$item->id, $item->name, Date :: format($item->date)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<?php
|
||||
|
||||
use EesyPHP\App;
|
||||
use EesyPHP\Db;
|
||||
use EesyPHP\SentrySpan;
|
||||
|
||||
use EesyPHPExample\Db;
|
||||
|
||||
use function EesyPHP\___;
|
||||
|
||||
error_reporting(E_ALL & ~E_NOTICE & ~E_STRICT & ~E_DEPRECATED);
|
||||
|
@ -70,7 +69,7 @@ foreach($status_list as $key => $value)
|
|||
|
||||
require_once('cli.php');
|
||||
require_once('templates.php');
|
||||
require_once('url-helpers.php');
|
||||
require_once('views/index.php');
|
||||
|
||||
Db :: init();
|
||||
|
||||
|
|
|
@ -77,10 +77,10 @@ function handle_item_post_data(&$info, $enabled_fields=null, $required_fields=nu
|
|||
}
|
||||
}
|
||||
|
||||
if (empty($field_errors) && is_array($item) && !is_null($changes)) {
|
||||
if (empty($field_errors) && $item && !is_null($changes)) {
|
||||
$changes = array();
|
||||
foreach ($info as $key => $value) {
|
||||
if ($value != $item[$key])
|
||||
if ($value != $item->$key)
|
||||
$changes[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
@ -113,7 +113,7 @@ function can_delete($item) {
|
|||
}
|
||||
|
||||
function can_do($item, $status=array()) {
|
||||
return in_array($item['status'], $status);
|
||||
return in_array($item->status, $status);
|
||||
}
|
||||
|
||||
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|
||||
|
|
|
@ -44,16 +44,16 @@ function smarty_item_status($params) {
|
|||
'refused' => 'danger',
|
||||
'archived' => 'secondary',
|
||||
);
|
||||
if (array_key_exists($params['item']['status'], $status2class)) {
|
||||
$class = $status2class[$params['item']['status']];
|
||||
if (array_key_exists($params['item']->status, $status2class)) {
|
||||
$class = $status2class[$params['item']->status];
|
||||
}
|
||||
else
|
||||
$class='danger';
|
||||
echo "<span class='badge bg-$class'>";
|
||||
echo (
|
||||
array_key_exists($params['item']['status'], $status_list)?
|
||||
$status_list[$params['item']['status']]:
|
||||
"Inconnu (".$params['item']['status'].")"
|
||||
array_key_exists($params['item']->status, $status_list)?
|
||||
$status_list[$params['item']->status]:
|
||||
"Inconnu (".$params['item']->status.")"
|
||||
);
|
||||
echo "</span>";
|
||||
}
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
<?php
|
||||
|
||||
use EesyPHP\Check;
|
||||
use EesyPHP\Log;
|
||||
use EesyPHP\Tpl;
|
||||
|
||||
use EesyPHPExample\Db;
|
||||
|
||||
function get_item_from_url($id, $fatal=false) {
|
||||
if (!Check :: id($id))
|
||||
Log :: fatal(_('Invalid element identifier.'));
|
||||
|
||||
$item = Db :: get_item($id);
|
||||
if(!is_array($item)) {
|
||||
$error = sprintf(_("Item #%s not found."), $id);
|
||||
if ($fatal)
|
||||
Log :: fatal($error);
|
||||
Tpl :: add_error($error);
|
||||
return false;
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|
|
@ -5,13 +5,14 @@ use EesyPHP\Log;
|
|||
use EesyPHP\Tpl;
|
||||
use EesyPHP\Url;
|
||||
|
||||
use EesyPHPExample\Db;
|
||||
use EesyPHPExample\Db\Item;
|
||||
|
||||
use function EesyPHP\vardump;
|
||||
|
||||
if (php_sapi_name() == "cli")
|
||||
return true;
|
||||
|
||||
|
||||
/**
|
||||
* Search page handler
|
||||
* @param EesyPHP\UrlRequest $request
|
||||
|
@ -28,7 +29,7 @@ function handle_search($request) {
|
|||
) {
|
||||
$_SESSION['search']=array(
|
||||
'pattern' => false,
|
||||
'status' => 'all',
|
||||
'filters' => ['status' => 'all'],
|
||||
'order' => 'name',
|
||||
'order_direction' => 'ASC',
|
||||
);
|
||||
|
@ -40,7 +41,7 @@ function handle_search($request) {
|
|||
$status_list['all'] = _('Any');
|
||||
if (isset($_REQUEST['status'])) {
|
||||
if (check_status($_REQUEST['status']) || $_REQUEST['status'] == 'all')
|
||||
$_SESSION['search']['status'] = $_REQUEST['status'];
|
||||
$_SESSION['search']['filters']['status'] = $_REQUEST['status'];
|
||||
else
|
||||
Tpl :: assign('status_error', true);
|
||||
}
|
||||
|
@ -82,7 +83,7 @@ function handle_search($request) {
|
|||
}
|
||||
|
||||
// Nb par page
|
||||
$nbs_by_page=array(10,25,50,100,500);
|
||||
$nbs_by_page = array(10,25,50,100,500);
|
||||
if (isset($_REQUEST['nb_by_page']) && in_array(intval($_REQUEST['nb_by_page']),$nbs_by_page)) {
|
||||
$_SESSION['search']['nb_by_page']=intval($_REQUEST['nb_by_page']);
|
||||
$_SESSION['search']['page']=1;
|
||||
|
@ -92,7 +93,7 @@ function handle_search($request) {
|
|||
}
|
||||
|
||||
Log :: debug('Search params : '.vardump($_SESSION['search']));
|
||||
$result = Db :: search_items($_SESSION['search']);
|
||||
$result = Item :: search($_SESSION['search']);
|
||||
if (!is_array($result))
|
||||
Tpl :: fatal_error(
|
||||
_("An error occurred while listing the items. ".
|
||||
|
@ -123,7 +124,7 @@ Url :: add_url_handler('|^item/?$|', 'handle_search');
|
|||
* @return void
|
||||
*/
|
||||
function handle_show($request) {
|
||||
$item = get_item_from_url($request -> id);
|
||||
$item = Item :: get_from_url($request -> id);
|
||||
if (!$item)
|
||||
Url :: error_404();
|
||||
|
||||
|
@ -138,10 +139,7 @@ function handle_show($request) {
|
|||
'js/myconfirm.js',
|
||||
);
|
||||
|
||||
Tpl :: display(
|
||||
"show.tpl", _("Element %s"),
|
||||
(is_array($item)?$item['name']:"#".$request -> id)
|
||||
);
|
||||
Tpl :: display("show.tpl", _("Element %s"), $item->name);
|
||||
}
|
||||
Url :: add_url_handler('|^item/(?P<id>[0-9]+)$|', 'handle_show');
|
||||
|
||||
|
@ -156,10 +154,11 @@ function handle_create($request) {
|
|||
$info = array();
|
||||
$field_errors = handle_item_post_data($info);
|
||||
if (isset($_POST['submit']) && empty($field_errors)) {
|
||||
$item = Db :: add_item($info);
|
||||
if (is_array($item)) {
|
||||
Tpl :: add_message(_("The element '%s' has been created."), $item['name']);
|
||||
Url :: redirect('item/'.$item['id']);
|
||||
$item = new Item();
|
||||
$item -> apply($info);
|
||||
if ($item->save()) {
|
||||
Tpl :: add_message(_("The element '%s' has been created."), $item->name);
|
||||
Url :: redirect('item/'.$item->id);
|
||||
}
|
||||
Tpl :: add_error(_("An error occurred while saving this item."));
|
||||
}
|
||||
|
@ -181,29 +180,30 @@ Url :: add_url_handler('|^item/new$|', 'handle_create');
|
|||
function handle_modify($request) {
|
||||
global $status_list;
|
||||
|
||||
$item = get_item_from_url($request -> id);
|
||||
if(!is_array($item))
|
||||
$item = Item :: get_from_url($request -> id);
|
||||
if(!$item)
|
||||
Url :: error_404();
|
||||
if (!can_modify($item)) {
|
||||
Tpl :: add_error(_('You cannot edit this item.'));
|
||||
Url :: redirect('item/'.$item['id']);
|
||||
Url :: redirect('item/'.$item->id);
|
||||
}
|
||||
$info = array();
|
||||
$field_errors = handle_item_post_data($info);
|
||||
if (isset($_POST['submit']) && empty($field_errors)) {
|
||||
$changes = array();
|
||||
foreach ($info as $key => $value) {
|
||||
if ($value != $item[$key])
|
||||
if ($value != $item->$key)
|
||||
$changes[$key] = $value;
|
||||
}
|
||||
Log :: debug('Changes : '.vardump($changes));
|
||||
if (empty($changes)) {
|
||||
Tpl :: add_message(_("You have not made any changes to element '%s'."), $item['name']);
|
||||
Url :: redirect('item/'.$item['id']);
|
||||
Tpl :: add_message(_("You have not made any changes to element '%s'."), $item->name);
|
||||
Url :: redirect('item/'.$item->id);
|
||||
}
|
||||
if (Db :: update_item($item['id'], $changes) === true) {
|
||||
Tpl :: add_message(_("The element '%s' has been updated successfully."), $item['name']);
|
||||
Url :: redirect('item/'.$item['id']);
|
||||
$item->apply($changes);
|
||||
if ($item->save()) {
|
||||
Tpl :: add_message(_("The element '%s' has been updated successfully."), $item->name);
|
||||
Url :: redirect('item/'.$item->id);
|
||||
}
|
||||
Tpl :: add_error(_("An error occurred while updating this item."));
|
||||
}
|
||||
|
@ -215,11 +215,11 @@ function handle_modify($request) {
|
|||
_("There are errors preventing this item from being saved. ".
|
||||
"Please correct them before attempting to save your changes."));
|
||||
Tpl :: assign('info', (!empty($info)?$info:$item));
|
||||
Tpl :: assign('item_id', $item['id']);
|
||||
Tpl :: assign('item_id', $item->id);
|
||||
Tpl :: assign('field_errors', $field_errors);
|
||||
Tpl :: assign('status_list', $status_list);
|
||||
|
||||
Tpl :: display("form.tpl", _("Element %s: Modification"), $item['name']);
|
||||
Tpl :: display("form.tpl", _("Element %s: Modification"), $item->name);
|
||||
}
|
||||
Url :: add_url_handler('|^item/(?P<id>[0-9]+)/modify$|', 'handle_modify');
|
||||
|
||||
|
@ -229,24 +229,24 @@ Url :: add_url_handler('|^item/(?P<id>[0-9]+)/modify$|', 'handle_modify');
|
|||
* @return never
|
||||
*/
|
||||
function handle_archive($request) {
|
||||
$item = get_item_from_url($request -> id);
|
||||
if(!is_array($item)) {
|
||||
$item = Item :: get_from_url($request -> id);
|
||||
if(!$item) {
|
||||
Tpl :: add_error(_("Item #%s not found."), $request -> id);
|
||||
Url :: redirect('item');
|
||||
}
|
||||
if ($item['status'] == 'archived') {
|
||||
if ($item->status == 'archived') {
|
||||
Tpl :: add_message(_("This item is already archived."));
|
||||
}
|
||||
else if (!can_archive($item)) {
|
||||
Tpl :: add_error(_('You cannot archive this item.'));
|
||||
}
|
||||
else if (Db :: archive_item($item['id']) === true) {
|
||||
Tpl :: add_message(_("The element '%s' has been archived successfully."), $item['name']);
|
||||
else if ($item->archive()) {
|
||||
Tpl :: add_message(_("The element '%s' has been archived successfully."), $item->name);
|
||||
}
|
||||
else {
|
||||
Tpl :: add_error(_('An error occurred while archiving this item.'));
|
||||
}
|
||||
Url :: redirect('item/'.$item['id']);
|
||||
Url :: redirect('item/'.$item->id);
|
||||
}
|
||||
Url :: add_url_handler('|^item/(?P<id>[0-9]+)/archive$|', 'handle_archive');
|
||||
|
||||
|
@ -256,19 +256,19 @@ Url :: add_url_handler('|^item/(?P<id>[0-9]+)/archive$|', 'handle_archive');
|
|||
* @return void
|
||||
*/
|
||||
function handle_delete($request) {
|
||||
$item = get_item_from_url($request -> id);
|
||||
if(!is_array($item)) {
|
||||
$item = Item :: get_from_url($request -> id);
|
||||
if(!$item) {
|
||||
Tpl :: add_error(_("Item #%s not found."), $request -> id);
|
||||
}
|
||||
else if (!can_delete($item)) {
|
||||
Tpl :: add_error(_('You cannot delete this item.'));
|
||||
}
|
||||
else if (Db :: delete_item($item['id']) === true) {
|
||||
Tpl :: add_message(_("The element '%s' has been deleted successfully."), $item['name']);
|
||||
else if ($item->delete()) {
|
||||
Tpl :: add_message(_("The element '%s' has been deleted successfully."), $item->name);
|
||||
}
|
||||
else {
|
||||
Tpl :: add_error(_('An error occurred while deleting this item.'));
|
||||
Url :: redirect('item/'.$item['id']);
|
||||
Url :: redirect('item/'.$item->id);
|
||||
}
|
||||
Url :: redirect('item');
|
||||
}
|
|
@ -1,374 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHPExample;
|
||||
|
||||
use EesyPHP\App;
|
||||
use EesyPHP\Hook;
|
||||
use EesyPHP\Log;
|
||||
use Unidecode\Unidecode;
|
||||
|
||||
use Exception;
|
||||
|
||||
class Db extends \EesyPHP\Db {
|
||||
/*
|
||||
* Methods to handle items
|
||||
*/
|
||||
static public function get_items($orderby='id', $raw_values=false) {
|
||||
try {
|
||||
$info = self :: get_many('item', null, null, $orderby);
|
||||
if (!is_array($info))
|
||||
return;
|
||||
if ($raw_values)
|
||||
return $info;
|
||||
|
||||
$items = array();
|
||||
foreach ($info as $item)
|
||||
$items[$item['id']] = self :: format_row_info($item, array('date'));
|
||||
return $items;
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error("Error retrieving items info from database : ".$e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function get_item($id, $raw_values=false) {
|
||||
try {
|
||||
$info = self :: get_one('item', array('id' => $id));
|
||||
|
||||
if (!is_array($info))
|
||||
return false;
|
||||
|
||||
if ($raw_values)
|
||||
return $info;
|
||||
|
||||
return self :: format_row_info($info, array('date'));
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error("Error retrieving item #$id info from database : ".$e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function add_item($values) {
|
||||
$values['date'] = self :: time2datetime(time());
|
||||
try {
|
||||
$result = self :: $fpdo -> insertInto('item')
|
||||
-> values($values)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
$item = self :: get_item($result);
|
||||
Log :: info("New item #$result added");
|
||||
Hook :: trigger('item_added', $item);
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error("Error creating item in database : ".$e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function update_item($id, $changes) {
|
||||
if (!is_array($changes))
|
||||
return false;
|
||||
if (empty($changes))
|
||||
return true;
|
||||
$item = self :: get_item($id, true);
|
||||
if (!is_array($item)) return false;
|
||||
|
||||
if (isset($changes['date']) && $changes['date'])
|
||||
$changes['date'] = self :: time2datetime($changes['date']);
|
||||
|
||||
try {
|
||||
$result = self :: $fpdo -> update('item')
|
||||
-> set($changes)
|
||||
-> where('id', $id)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
Log :: info("Item #$id updated");
|
||||
Hook :: trigger('item_updated', $item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error("Error updating item #$id in database : ".$e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function change_item_status($id, $status) {
|
||||
if (self :: update_item($id, array('status' => $status))) {
|
||||
Log :: info("Status of item #$id changed to $status.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function archive_item($id) {
|
||||
return self :: change_item_status($id, 'archived');
|
||||
}
|
||||
|
||||
static public function delete_item($id) {
|
||||
try {
|
||||
$result = self :: $fpdo -> deleteFrom('item')
|
||||
-> where('id', $id)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
Log :: info("Item #$id deleted");
|
||||
return True;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error("Error deleting item #$id from database : ".$e->getMessage());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function search_items($params) {
|
||||
// Detect PgSQL backend
|
||||
$is_pgsql = (strpos(App::get('db.dsn', '', 'string'), "pgsql:") === 0);
|
||||
|
||||
$where = array();
|
||||
if (isset($params['status']) && $params['status'] && $params['status'] != 'all')
|
||||
$where['status'] = $params['status'];
|
||||
|
||||
$patterns_where = array();
|
||||
if (isset($params['pattern']) && $params['pattern']) {
|
||||
foreach(preg_split('/\s+/', trim($params['pattern'])) as $word) {
|
||||
if (!$word) continue;
|
||||
$patterns_word=array();
|
||||
|
||||
// If numeric pattern ?
|
||||
if (preg_match('/^[0-9]+$/', $word)) {
|
||||
// Numeric pattern
|
||||
$word = intval($word);
|
||||
$patterns_word['id = ?'] = $word;
|
||||
}
|
||||
else {
|
||||
// Text pattern
|
||||
foreach (array('name', 'description') as $field) {
|
||||
if ($is_pgsql) {
|
||||
$word = Unidecode::unidecode($word);
|
||||
$patterns_word["unaccent($field) ILIKE ?"] = "%$word%";
|
||||
}
|
||||
else
|
||||
$patterns_word["$field LIKE ?"] = "%$word%";
|
||||
}
|
||||
}
|
||||
$patterns_where[] = $patterns_word;
|
||||
}
|
||||
}
|
||||
|
||||
$order='id';
|
||||
$orders=array('id', 'name', 'date', 'status', 'description');
|
||||
if (isset($params['order'])) {
|
||||
if (!in_array($order, $orders))
|
||||
return -1;
|
||||
$order=$params['order'];
|
||||
}
|
||||
|
||||
$order_direction='DESC';
|
||||
if (isset($params['order_direction']) && $params['order_direction']) {
|
||||
if (!in_array($params['order_direction'], array('ASC', 'DESC')))
|
||||
return -1;
|
||||
$order_direction=$params['order_direction'];
|
||||
}
|
||||
|
||||
$orderby="$order $order_direction";
|
||||
|
||||
$limit = "";
|
||||
$page = 1;
|
||||
$nb_by_page = 10;
|
||||
$offset = 0;
|
||||
if (!isset($params['all'])) {
|
||||
if (isset($params['page']) && $params['page']>0) {
|
||||
if (isset($params['nb_by_page']) && $params['nb_by_page']>0) {
|
||||
$nb_by_page = intval($params['nb_by_page']);
|
||||
}
|
||||
$page = intval($params['page']);
|
||||
}
|
||||
$offset = ($page-1)*$nb_by_page;
|
||||
$limit = $nb_by_page;
|
||||
}
|
||||
|
||||
try {
|
||||
$query = self :: $fpdo -> from('item');
|
||||
if (!empty($where))
|
||||
$query -> where($where);
|
||||
foreach ($patterns_where as $patterns_word)
|
||||
call_user_func_array(
|
||||
array($query, 'where'),
|
||||
array_merge(
|
||||
array('('.implode(' OR ', array_keys($patterns_word)).')'),
|
||||
array_values($patterns_word)
|
||||
)
|
||||
);
|
||||
$result = $query -> orderBy($orderby)
|
||||
-> limit($limit)
|
||||
-> offset($offset)
|
||||
-> execute();
|
||||
|
||||
if ($result === false) {
|
||||
Log :: error('search_items() : search in DB return false');
|
||||
return false;
|
||||
}
|
||||
|
||||
$rows = $result -> fetchAll();
|
||||
$items = array();
|
||||
foreach ($rows as $row) {
|
||||
$items[] = self :: format_row_info($row, array('date'));
|
||||
}
|
||||
if (isset($params['all'])) {
|
||||
return array(
|
||||
'count' => count($items),
|
||||
'first' => 1,
|
||||
'last' => count($items),
|
||||
'nb_pages' => 1,
|
||||
'page' => 1,
|
||||
'items' => $items
|
||||
);
|
||||
}
|
||||
$query_count = self :: $fpdo -> from('item')
|
||||
-> select(null)
|
||||
-> select('count(*) as count');
|
||||
if (!empty($where))
|
||||
$query_count -> where($where);
|
||||
foreach ($patterns_where as $patterns_word)
|
||||
call_user_func_array(
|
||||
array($query_count, 'where'),
|
||||
array_merge(
|
||||
array('('.implode(' OR ', array_keys($patterns_word)).')'),
|
||||
array_values($patterns_word)
|
||||
)
|
||||
);
|
||||
|
||||
$result_count = $query_count -> execute();
|
||||
|
||||
if ($result_count === false) {
|
||||
Log :: debug('search_items() : search for count in DB return false');
|
||||
return False;
|
||||
}
|
||||
$count = $result_count -> fetch();
|
||||
return array(
|
||||
'count' => $count['count'],
|
||||
'first' => $offset+1,
|
||||
'last' => (
|
||||
$offset+$nb_by_page<$count['count']?
|
||||
$offset+$nb_by_page:$count['count']),
|
||||
'nb_pages' => ceil($count['count']/$nb_by_page),
|
||||
'page' => $page,
|
||||
'items' => $items,
|
||||
);
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: exception(
|
||||
$e, "An exception occurred searching items with params %s infos from database : ",
|
||||
preg_replace("/\n[ \t]*/", " ", print_r($params, true))
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static public function export_items($fd=null) {
|
||||
if (!$fd) $fd = fopen('php://output', 'w');
|
||||
fputcsv(
|
||||
$fd,
|
||||
array (
|
||||
'id',
|
||||
'name',
|
||||
'date',
|
||||
'status',
|
||||
'description',
|
||||
)
|
||||
);
|
||||
$items = self :: get_items();
|
||||
foreach($items as $item) {
|
||||
fputcsv(
|
||||
$fd,
|
||||
array(
|
||||
$item['id'],
|
||||
$item['name'],
|
||||
$item['date'],
|
||||
$item['status'],
|
||||
$item['description'],
|
||||
)
|
||||
);
|
||||
}
|
||||
return True;
|
||||
}
|
||||
|
||||
static public function restore_items($fd=null) {
|
||||
if (!$fd) $fd = fopen('php://stdin', 'r');
|
||||
try {
|
||||
$result = self :: $fpdo -> deleteFrom('item')
|
||||
-> execute();
|
||||
if ($result === false) {
|
||||
Log :: error("An unknown error occurred truncating item table in database.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error("Error truncating item table in database : ".$e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$first_row = false;
|
||||
$line = 0;
|
||||
$restored = 0;
|
||||
$error = false;
|
||||
$datetime_fields = array (
|
||||
'date',
|
||||
);
|
||||
// Map fields to support hold CSV files format
|
||||
$mapping = array (
|
||||
'creation_date' => 'date',
|
||||
'desc' => 'description',
|
||||
);
|
||||
|
||||
while (($row = fgetcsv($fd)) !== FALSE) {
|
||||
$line++;
|
||||
if ($first_row === false) {
|
||||
$first_row = $row;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$values = array();
|
||||
for ($i=0; $i < count($first_row); $i++) {
|
||||
if (!$row[$i]) continue;
|
||||
$field = (
|
||||
array_key_exists($first_row[$i], $mapping)?
|
||||
$mapping[$first_row[$i]]:
|
||||
$first_row[$i]);
|
||||
$value = (in_array($field, $datetime_fields)?self :: time2datetime($row[$i]):$row[$i]);
|
||||
$values[$field] = $value;
|
||||
}
|
||||
$result = self :: $fpdo -> insertInto('item')
|
||||
-> values($values)
|
||||
-> execute();
|
||||
|
||||
if ($result !== false) {
|
||||
$restored++;
|
||||
}
|
||||
else {
|
||||
Log :: error("Unknown error occurred restoring item from line #$line :\n".print_r($values, true));
|
||||
$error = true;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error(
|
||||
"Error restoring item from line #$line : ".$e->getMessage()."\n".print_r($values, true));
|
||||
$error = true;
|
||||
}
|
||||
}
|
||||
Log :: info("$restored items restored");
|
||||
|
||||
// Trigger hooks
|
||||
Hook :: trigger('items_restored');
|
||||
|
||||
return !$error;
|
||||
}
|
||||
}
|
195
example/src/Db/Item.php
Normal file
195
example/src/Db/Item.php
Normal file
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHPExample\Db;
|
||||
|
||||
use EesyPHP\Check;
|
||||
use EesyPHP\Hook;
|
||||
use EesyPHP\Log;
|
||||
use EesyPHP\Tpl;
|
||||
|
||||
use EesyPHP\Db\AttrBool;
|
||||
use EesyPHP\Db\AttrInt;
|
||||
use EesyPHP\Db\AttrStr;
|
||||
use EesyPHP\Db\AttrTimestamp;
|
||||
use EesyPHP\Db\DbObject;
|
||||
|
||||
use EesyPHP\Export\CSV;
|
||||
|
||||
use Unidecode\Unidecode;
|
||||
|
||||
/**
|
||||
* Item database object
|
||||
* @property int|null $id
|
||||
* @property string $name
|
||||
* @property \DateTime|null $date
|
||||
* @property string $status
|
||||
* @property string|null $description
|
||||
*/
|
||||
class Item extends DbObject {
|
||||
protected const TABLE = 'item';
|
||||
protected const PRIMARY_KEYS = ['id'];
|
||||
protected const DEFAULT_ORDER = 'id';
|
||||
protected const DEFAULT_ORDER_DIRECTION = 'DESC';
|
||||
protected const POSSIBLE_ORDERS = ['id', 'name', 'date', 'status'];
|
||||
|
||||
protected static function get_schema() {
|
||||
return [
|
||||
'id' => new AttrInt(['autoincrement' => true]),
|
||||
'name' => new AttrStr(['required' => true]),
|
||||
'date' => new AttrTimestamp(['default' => 'time']),
|
||||
'status' => new AttrStr(['required' => true, 'default' => 'pending']),
|
||||
'description' => new AttrStr(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get item from URL
|
||||
* @param mixed $id Item ID as retrieved from URL
|
||||
* @param boolean $fatal Set to true to trigger fatal error if item is not found in DB
|
||||
* @return Item|false
|
||||
*/
|
||||
public static function get_from_url($id, $fatal=false) {
|
||||
if (!Check :: id($id))
|
||||
Log :: fatal(_('Invalid element identifier.'));
|
||||
|
||||
$item = self :: get($id);
|
||||
if(!$item instanceof Item) {
|
||||
$error = sprintf(_("Item #%s not found."), $id);
|
||||
if ($fatal)
|
||||
Log :: fatal($error);
|
||||
Tpl :: add_error($error);
|
||||
return false;
|
||||
}
|
||||
return $item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change item status in DB
|
||||
* @param string $status New item status
|
||||
* @return bool
|
||||
*/
|
||||
public function change_status($status) {
|
||||
$this->status = $status;
|
||||
if ($this -> save()) {
|
||||
Log :: info("Status of item #$this->id changed to $status.");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set item as archived in DB
|
||||
* @return bool
|
||||
*/
|
||||
public function archive() {
|
||||
return $this -> change_status('archived');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Compute WHERE clauses from word pattern
|
||||
* @throws \EesyPHP\Db\DbException
|
||||
* @return array
|
||||
*/
|
||||
public static function word_to_filters($word) {
|
||||
$patterns_word = [];
|
||||
// If numeric pattern ?
|
||||
if (preg_match('/^[0-9]+$/', $word)) {
|
||||
// Numeric pattern
|
||||
$word = intval($word);
|
||||
$patterns_word['id = ?'] = $word;
|
||||
}
|
||||
else {
|
||||
// Text pattern
|
||||
foreach (array('name', 'description') as $field) {
|
||||
if (self :: DB_CLASS :: is_pgsql()) {
|
||||
$word = Unidecode::unidecode($word);
|
||||
$patterns_word["unaccent($field) ILIKE ?"] = "%$word%";
|
||||
}
|
||||
else
|
||||
$patterns_word["$field LIKE ?"] = "%$word%";
|
||||
}
|
||||
}
|
||||
return $patterns_word;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search objects
|
||||
* @throws \EesyPHP\Db\DbException
|
||||
* @see \EesyPHP\Db\DbObject::search()
|
||||
* @return array|false The search result as an array, or False in case of error.
|
||||
*/
|
||||
public static function search($params) {
|
||||
if (
|
||||
$params &&
|
||||
isset($params['filters']) &&
|
||||
isset($params['filters']['status']) &&
|
||||
$params['filters']['status'] == 'all'
|
||||
)
|
||||
unset($params['filters']['status']);
|
||||
return parent :: search($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export items
|
||||
* @param resource|null $fd The file pointer where to export (optional, default: php://output)
|
||||
* @param array<int,array<string,mixed>>|null $items Items to export
|
||||
* @param array<string,\EesyPHP\Db\Attr> $schema The object schema (optional)
|
||||
* @return boolean
|
||||
*/
|
||||
static function export($fd=null, $items=null, $schema=null) {
|
||||
$schema = $schema ? $schema : static :: get_schema();
|
||||
$export = new CSV(static :: csv_export_fields($schema));
|
||||
$items = $items?$items:static :: list();
|
||||
if ($items === false)
|
||||
return false;
|
||||
$rows = [];
|
||||
foreach ($items as $item) {
|
||||
$row = [];
|
||||
foreach($schema as $attr => $attr_type)
|
||||
$row[$attr] = $item->$attr;
|
||||
$rows[] = $row;
|
||||
}
|
||||
return $export->export($rows, $fd);
|
||||
}
|
||||
|
||||
static function restore($fd=null, $schema=null) {
|
||||
$schema = $schema ? $schema : static :: get_schema();
|
||||
$import = new CSV(static :: csv_export_fields($schema));
|
||||
$items = $import->load($fd);
|
||||
if ($items === false) {
|
||||
Log :: error("Error loading items.");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$items) {
|
||||
Log :: error("No item loaded.");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (self :: DB_CLASS :: truncate(self :: TABLE)) {
|
||||
Log :: debug("Table %s truncated", self :: TABLE);
|
||||
}
|
||||
else {
|
||||
Log :: error("An unknown error occurred truncating table %s in database.", self :: TABLE);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
Log :: error("Error truncating item table in database : ".$e->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$success = true;
|
||||
foreach($items as $item) {
|
||||
$obj = new Item();
|
||||
$obj->apply($item);
|
||||
$success = $success && $obj->save();
|
||||
}
|
||||
|
||||
// Trigger hooks
|
||||
Hook :: trigger('items_restored');
|
||||
return $success;
|
||||
}
|
||||
}
|
|
@ -7,9 +7,11 @@
|
|||
<label class="col-sm-2 col-form-label required">{t}Name{/t}</label>
|
||||
<div class="col-sm-10">
|
||||
<input name="name" type="text" required
|
||||
value='{if array_key_exists('name', $info) && $info.name}{$info.name|escape:'quotes'}{/if}'
|
||||
value='{if $info->name}{$info->name|escape:'quotes'}{/if}'
|
||||
class="form-control{if array_key_exists('name', $field_errors)} is-invalid{else if $submitted} is-valid{/if}"/>
|
||||
{if array_key_exists('name', $field_errors)}<div class="invalid-feedback">{$field_errors['name']}</div>{/if}
|
||||
{if array_key_exists('name', $field_errors)}
|
||||
<div class="invalid-feedback">{$field_errors['name']}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -18,22 +20,30 @@
|
|||
<div class="col-sm-10">
|
||||
<select name="status" id="status" required
|
||||
class="form-select{if array_key_exists('status', $field_errors)} is-invalid{else if $submitted} is-valid{/if}">
|
||||
{html_options options=$status_list selected=$info.status}
|
||||
{html_options options=$status_list selected=$info->status}
|
||||
</select>
|
||||
{if array_key_exists('status', $field_errors)}<div class="invalid-feedback">{$field_errors['status']}</div>{/if}
|
||||
{if array_key_exists('status', $field_errors)}
|
||||
<div class="invalid-feedback">{$field_errors['status']}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label class="col-sm-2 col-form-label">{t}Description{/t}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea name="description" id="description" class="form-control{if array_key_exists('description', $field_errors)} is-invalid{else if $submitted} is-valid{/if}">{if array_key_exists('description', $info) && $info.description}{$info.description|escape:"htmlall"}{/if}</textarea>
|
||||
{if array_key_exists('description', $field_errors)}<div class="form-error">{$field_errors['description']}</div>{/if}
|
||||
<textarea name="description" id="description"
|
||||
class="form-control{if array_key_exists('description', $field_errors)} is-invalid{else if $submitted} is-valid{/if}"
|
||||
>{if $info->description}{$info->description|escape:"htmlall"}{/if}</textarea>
|
||||
{if array_key_exists('description', $field_errors)}
|
||||
<div class="form-error">{$field_errors['description']}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="center">
|
||||
<a href="{if isset($item_id)}item/{$item_id}{else}search{/if}" class="btn btn-light"><i class="fa fa-undo"></i> {t}Back{/t}</a>
|
||||
<a href="{if isset($item_id)}item/{$item_id}{else}search{/if}" class="btn btn-light">
|
||||
<i class="fa fa-undo"></i> {t}Back{/t}
|
||||
</a>
|
||||
<button type="submit" name="submit" value="submitted" class="btn btn-primary">
|
||||
{if isset($item_id)}
|
||||
<i class="fa fa-floppy-o" aria-hidden="true"></i> {t}Save{/t}
|
||||
|
|
|
@ -13,8 +13,9 @@
|
|||
<div class="col">
|
||||
<div class="input-group mb-3">
|
||||
<label class="input-group-text" for="status">{t}Status{/t}</label>
|
||||
<select name="status" id="status" class="form-select{if isset($status_error) && $status_error} is-invalid{/if}">
|
||||
{html_options options=$status_list selected=$search.status}
|
||||
<select name="status" id="status"
|
||||
class="form-select{if isset($status_error) && $status_error} is-invalid{/if}">
|
||||
{html_options options=$status_list selected=$search.filters.status}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,8 +32,12 @@
|
|||
<div class="col-md-3">
|
||||
<div class="container">
|
||||
<div class="btn-group" role="group" aria-label="Actions">
|
||||
<button type="submit" name="submit" class="btn btn-primary"><i class="fa fa-search"></i> {t}Search{/t}</button>
|
||||
<a href="{$request->current_url}?clear=true" class="btn btn-warning"><i class="fa fa-trash"></i> {t}Reset{/t}</a>
|
||||
<button type="submit" name="submit" class="btn btn-primary">
|
||||
<i class="fa fa-search"></i> {t}Search{/t}
|
||||
</button>
|
||||
<a href="{$request->current_url}?clear=true" class="btn btn-warning">
|
||||
<i class="fa fa-trash"></i> {t}Reset{/t}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -46,8 +51,12 @@
|
|||
<table class="table" style='margin-top: 1em;'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{table_ordered_th url="{$request->current_url}" order="date" text="{t}Date{/t}" search=$search}</th>
|
||||
<th scope="col">{table_ordered_th url="{$request->current_url}" order="name" text="{t}Name{/t}" search=$search}</th>
|
||||
<th scope="col">
|
||||
{table_ordered_th url="{$request->current_url}" order="date" text="{t}Date{/t}" search=$search}
|
||||
</th>
|
||||
<th scope="col">
|
||||
{table_ordered_th url="{$request->current_url}" order="name" text="{t}Name{/t}" search=$search}
|
||||
</th>
|
||||
<th scope="col">{t}Status{/t}</th>
|
||||
<th scope="col" class="center">{t}Actions{/t}</th>
|
||||
</tr>
|
||||
|
@ -55,14 +64,36 @@
|
|||
<tbody>
|
||||
{foreach $result.items as $item}
|
||||
<tr>
|
||||
<td>{format_time time=$item.date}</td>
|
||||
<td><a href="item/{$item.id}"><span title='{$item.id|escape:'quotes'}'>{$item.name}</span></a></td>
|
||||
<td>{format_time time=$item->date}</td>
|
||||
<td>
|
||||
<a href="item/{$item->id}">
|
||||
<span title='{$item->id|escape:'quotes'}'>{$item->name}</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>{item_status item=$item}</td>
|
||||
<td class="center">
|
||||
<a href="item/{$item.id}" class="btn btn-sm btn-info"><i class="fa fa-eye"></i> {t}View{/t}</a>
|
||||
{if can_modify($item)}<a href="item/{$item.id}/modify" class="btn btn-sm btn-primary"><i class="fa fa-edit"></i> {t}Modify{/t}</a>{/if}
|
||||
{if can_archive($item)}<a data-myconfirm-url="item/{$item.id}/archive" data-myconfirm-question="{t}Are you sure you want to archive this item?{/t}" class="btn btn-sm btn-warning myconfirm-link"><i class="fa fa-archive"></i> {t}Archive{/t}</a>{/if}
|
||||
{if can_delete($item)}<a href="#" data-myconfirm-url="item/{$item.id}/delete" data-myconfirm-question="{t}Are you sure you want to delete this item?{/t}" class="btn btn-sm btn-danger myconfirm-link"><i class="fa fa-trash"></i> {t}Delete{/t}</a>{/if}
|
||||
<a href="item/{$item->id}" class="btn btn-sm btn-info">
|
||||
<i class="fa fa-eye"></i> {t}View{/t}
|
||||
</a>
|
||||
{if can_modify($item)}
|
||||
<a href="item/{$item->id}/modify" class="btn btn-sm btn-primary">
|
||||
<i class="fa fa-edit"></i> {t}Modify{/t}
|
||||
</a>
|
||||
{/if}
|
||||
{if can_archive($item)}
|
||||
<a data-myconfirm-url="item/{$item->id}/archive"
|
||||
data-myconfirm-question="{t}Are you sure you want to archive this item?{/t}"
|
||||
class="btn btn-sm btn-warning myconfirm-link">
|
||||
<i class="fa fa-archive"></i> {t}Archive{/t}
|
||||
</a>
|
||||
{/if}
|
||||
{if can_delete($item)}
|
||||
<a href="#" data-myconfirm-url="item/{$item->id}/delete"
|
||||
data-myconfirm-question="{t}Are you sure you want to delete this item?{/t}"
|
||||
class="btn btn-sm btn-danger myconfirm-link">
|
||||
<i class="fa fa-trash"></i> {t}Delete{/t}
|
||||
</a>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<label class="col-sm-4 col-form-label">{t}Name{/t}</label>
|
||||
<div class="col-sm-8">
|
||||
<span class="form-control-plaintext">
|
||||
{$item.name}
|
||||
{$item->name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -14,7 +14,7 @@
|
|||
<label class="col-sm-4 col-form-label">{t}Creation date{/t}</label>
|
||||
<div class="col-sm-8">
|
||||
<span class="form-control-plaintext">
|
||||
{format_time time=$item.date}
|
||||
{format_time time=$item->date}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<label class="col-sm-4 col-form-label">{t}Description{/t}</label>
|
||||
<div class="col-sm-8">
|
||||
<span class="form-control-plaintext">
|
||||
{if $item.description}{$item.description|escape:'htmlall'}{else}{t}Unspecified.{/t}{/if}
|
||||
{if $item->description}{$item->description|escape:'htmlall'}{else}{t}Unspecified.{/t}{/if}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,8 +40,8 @@
|
|||
|
||||
<div class="center">
|
||||
<a href="item" class="btn btn-light"><i class="fa fa-undo"></i> {t}Back{/t}</a>
|
||||
{if can_modify($item)}<a href="item/{$item.id}/modify" class="btn btn-primary"><i class="fa fa-edit"></i> {t}Modify{/t}</a>{/if}
|
||||
{if can_archive($item)}<a data-myconfirm-url="item/{$item.id}/archive" data-myconfirm-question="{t}Are you sure you want to archive this item?{/t}" class="btn btn-warning myconfirm-link"><i class="fa fa-archive"></i> {t}Archive{/t}</a>{/if}
|
||||
{if can_delete($item)}<a href="#" data-myconfirm-url="item/{$item.id}/delete" data-myconfirm-question="{t}Are you sure you want to delete this item?{/t}" class="btn btn-danger myconfirm-link"><i class="fa fa-trash"></i> {t}Delete{/t}</a>{/if}
|
||||
{if can_modify($item)}<a href="item/{$item->id}/modify" class="btn btn-primary"><i class="fa fa-edit"></i> {t}Modify{/t}</a>{/if}
|
||||
{if can_archive($item)}<a data-myconfirm-url="item/{$item->id}/archive" data-myconfirm-question="{t}Are you sure you want to archive this item?{/t}" class="btn btn-warning myconfirm-link"><i class="fa fa-archive"></i> {t}Archive{/t}</a>{/if}
|
||||
{if can_delete($item)}<a href="#" data-myconfirm-url="item/{$item->id}/delete" data-myconfirm-question="{t}Are you sure you want to delete this item?{/t}" class="btn btn-danger myconfirm-link"><i class="fa fa-trash"></i> {t}Delete{/t}</a>{/if}
|
||||
</div>
|
||||
{/block}
|
||||
|
|
|
@ -6,6 +6,7 @@ parameters:
|
|||
- EesyPHP\HookEvent
|
||||
- EesyPHP\UrlRequest
|
||||
- EesyPHP\Auth\User
|
||||
- EesyPHP\Db\DbObject
|
||||
ignoreErrors:
|
||||
-
|
||||
message: "#Property EesyPHP\\\\Auth\\\\Ldap::\\$connection has unknown class Net_LDAP2 as its type\\.#"
|
||||
|
@ -31,8 +32,3 @@ parameters:
|
|||
-
|
||||
message: "#Call to static method isError\\(\\) on an unknown class PEAR\\.#"
|
||||
path: src/Email.php
|
||||
-
|
||||
message: "#Variable \\$status_list might not be defined\\.#"
|
||||
paths:
|
||||
- example/includes/cli.php
|
||||
|
||||
|
|
101
src/Date.php
101
src/Date.php
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace EesyPHP;
|
||||
|
||||
|
||||
class Date {
|
||||
|
||||
/**
|
||||
|
@ -9,48 +10,98 @@ class Date {
|
|||
* @see strftime()
|
||||
* @var string
|
||||
*/
|
||||
public static $date_format = "%d/%m/%Y";
|
||||
public static $date_format = "d/m/Y";
|
||||
|
||||
/**
|
||||
* The datetime format
|
||||
* @see strftime()
|
||||
* @var string
|
||||
*/
|
||||
public static $date_time_format = "%d/%m/%Y %H:%M:%S";
|
||||
public static $date_time_format = "d/m/Y H:i:s";
|
||||
|
||||
/**
|
||||
* Format a timestamp as date/datetime string
|
||||
* @param int $time The timestamp to format
|
||||
* @param bool $with_time If true, include time in formatted string
|
||||
* (optional, default: true)
|
||||
* @param int|\DateTime $time The timestamp to format (or a DateTime object)
|
||||
* @param bool|string $with_time_or_format If true, include time in formatted string, if string
|
||||
* use it as date format (optional, default: true)
|
||||
* @return string
|
||||
*/
|
||||
public static function format($time, $with_time=true) {
|
||||
if ($with_time)
|
||||
return strftime(self :: $date_time_format, $time);
|
||||
return strftime(self :: $date_format, $time);
|
||||
public static function format($time, $with_time_or_format=true) {
|
||||
$format = (
|
||||
is_string($with_time_or_format)?
|
||||
$with_time_or_format:
|
||||
($with_time_or_format ? self :: $date_time_format : self :: $date_format)
|
||||
);
|
||||
return is_a($time, "DateTime")?$time->format($format):date($format, $time);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a date/datetime string as timestamp
|
||||
* @param string $date The date string to parse
|
||||
* @param bool $with_time If true, consider the date string included time
|
||||
* (optional, default: true)
|
||||
* @param bool|string $with_time_or_format If true, include time in formatted string, if string
|
||||
* use it as date format (optional, default: true)
|
||||
* @param \DateTimeZone|string|null $timezone Timezone of the loaded date
|
||||
* (optional, default: system)
|
||||
* @param bool $as_datetime If true, return a DateTime object instead of timestamp as integer
|
||||
* @return ( $as_datetime is true ? \DateTime : int )|false
|
||||
*/
|
||||
public static function parse($date, $with_time=true) {
|
||||
if ($with_time)
|
||||
$ptime = strptime($date, self :: $date_time_format);
|
||||
else
|
||||
$ptime = strptime($date, self :: $date_format);
|
||||
if(is_array($ptime)) {
|
||||
return mktime(
|
||||
$ptime['tm_hour'],
|
||||
$ptime['tm_min'],
|
||||
$ptime['tm_sec'],
|
||||
$ptime['tm_mon']+1,
|
||||
$ptime['tm_mday'],
|
||||
$ptime['tm_year']+1900
|
||||
public static function parse(
|
||||
$date, $with_time_or_format=true, $timezone=null, $as_datetime=false
|
||||
) {
|
||||
$format = (
|
||||
is_string($with_time_or_format)?
|
||||
$with_time_or_format:
|
||||
($with_time_or_format ? self :: $date_time_format : self :: $date_format)
|
||||
);
|
||||
$date = \DateTime::createFromFormat($format, $date, self :: timezone($timezone));
|
||||
if ($as_datetime || $date === false)
|
||||
return $date;
|
||||
return $date->getTimestamp();
|
||||
}
|
||||
return false;
|
||||
|
||||
/**
|
||||
* Get timezone
|
||||
* @param \DateTimeZone|string|null $value Timezone name or 'system'|null for system timezone
|
||||
* (optional)
|
||||
* @param bool $as_string If true, return the name of the timezone instead of a DateTimeZone
|
||||
* object (optional, default: false)
|
||||
* @return ($as_string is true ? string|false : \DateTimeZone|false)
|
||||
*/
|
||||
public static function timezone($value=null, $as_string=false) {
|
||||
$timezone = (
|
||||
is_null($value) || $value == 'system'?
|
||||
get_system_timezone():
|
||||
timezone_open($value)
|
||||
);
|
||||
if (!$as_string || !$timezone)
|
||||
return $timezone;
|
||||
return $timezone -> getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create \DateTime object from timestamp
|
||||
* @param int $value The timestamp
|
||||
* @param \DateTimeZone|string|null $timezone Timezone name or 'system'|null for system timezone
|
||||
* (optional)
|
||||
* @return \DateTime
|
||||
*/
|
||||
public static function from_timestamp($value, $timezone=null) {
|
||||
$date = new \DateTime();
|
||||
$date -> setTimestamp($value);
|
||||
$date -> setTimezone(self :: timezone($timezone));
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create \DateTime object
|
||||
* @param int|null $timezone The expected timezone (optional, default: system one)
|
||||
* @param \DateTimeZone|string|null $timezone Timezone name or 'system'|null for system timezone
|
||||
* (optional)
|
||||
* @return \DateTime
|
||||
*/
|
||||
public static function now($timezone=null) {
|
||||
$date = new \DateTime();
|
||||
$date -> setTimezone(self :: timezone($timezone));
|
||||
return $date;
|
||||
}
|
||||
}
|
||||
|
|
71
src/Db.php
71
src/Db.php
|
@ -427,6 +427,48 @@ class Db {
|
|||
return $expected_row_changes == $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to truncate a table of the database
|
||||
* Note: FluentPDO does not provide a way to execute TRUNCATE SQL query, directly use PDO object.
|
||||
* @param string $raw_table The table name
|
||||
* @return bool
|
||||
*/
|
||||
public static function truncate($raw_table) {
|
||||
Log :: debug(
|
||||
"truncate(%s): FluentPDO does not provide a way to execute TRUNCATE SQL query, ".
|
||||
"directly use PDO object.", $raw_table
|
||||
);
|
||||
$table = preg_replace('/[^A-Za-z0-9_]+/', '', $raw_table);
|
||||
if ($raw_table != $table)
|
||||
Log :: debug("truncate(%s):: Table name cleaned as '%s'", $raw_table, $table);
|
||||
try {
|
||||
if (self :: is_sqlite()) {
|
||||
Log :: debug(
|
||||
'truncate(%s): Sqlite does not support TRUNCATE SQL query, use DELETE instead.',
|
||||
$table
|
||||
);
|
||||
$statement = self :: $pdo -> prepare("DELETE FROM $table");
|
||||
}
|
||||
else {
|
||||
$statement = self :: $pdo -> prepare("TRUNCATE $table");
|
||||
}
|
||||
if (!$statement -> execute()) {
|
||||
Log :: error("Unknown error occurred truncating the table %s of the database", $table);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception $e) {
|
||||
Log :: error(
|
||||
"Error occurred truncating the table %s of the database: %s",
|
||||
$table,
|
||||
$e->getMessage()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
Log::info("Table %s truncated", $table);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Handle date/datetime format
|
||||
|
@ -487,10 +529,11 @@ class Db {
|
|||
|
||||
/**
|
||||
* Helper method to parse PHP PDO DSN info
|
||||
* @param string $dsn PHP PDO DSN to parse
|
||||
* @param string|null $dsn PHP PDO DSN to parse (optional)
|
||||
* @return array|false
|
||||
*/
|
||||
public static function parse_pdo_dsn($dsn) {
|
||||
public static function parse_pdo_dsn($dsn=null) {
|
||||
$dsn = $dsn?$dsn:App::get(static :: $config_prefix.".dsn");
|
||||
if (!preg_match('/^([^:]+):(.+)$/', $dsn, $dsn_parts))
|
||||
return false;
|
||||
|
||||
|
@ -502,4 +545,28 @@ class Db {
|
|||
Log::trace('parse_pdo_dsn(%s): %s', $dsn, vardump($info));
|
||||
return $info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current database type
|
||||
* @return string
|
||||
*/
|
||||
public static function get_db_type() {
|
||||
return static :: $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to test if we use a sqlite database
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_sqlite() {
|
||||
return static :: get_db_type() == 'sqlite';
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to test if we use a PostgreSQL database
|
||||
* @return bool
|
||||
*/
|
||||
public static function is_pgsql() {
|
||||
return static :: get_db_type() == 'pgsql';
|
||||
}
|
||||
}
|
||||
|
|
80
src/Db/Attr.php
Normal file
80
src/Db/Attr.php
Normal file
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
class Attr {
|
||||
|
||||
/**
|
||||
* Attribute default value
|
||||
* @var mixed
|
||||
*/
|
||||
protected $default = null;
|
||||
|
||||
/**
|
||||
* Attribute required flag
|
||||
* @var bool
|
||||
*/
|
||||
public $required = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param array<string,mixed>|null $parameters Attribute parameters
|
||||
*/
|
||||
public function __construct($parameters=null) {
|
||||
if (!is_array($parameters))
|
||||
return;
|
||||
foreach($parameters as $key => $value) {
|
||||
if (!property_exists($this, $key))
|
||||
throw new DbException("Attribute %s as no %s property", get_called_class(), $key);
|
||||
$this -> $key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute default value
|
||||
* @param mixed $value Override configured default value
|
||||
* @return mixed
|
||||
*/
|
||||
public function default($value=null) {
|
||||
$value = $value?$value:$this -> default;
|
||||
return is_callable($value)?$value():$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value from DB
|
||||
* @param mixed $value The value as retrieved from debug
|
||||
* @return mixed The attribute value
|
||||
*/
|
||||
public function from_db($value) {
|
||||
return is_null($value)?$this -> default():$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value for DB
|
||||
* @param mixed $value The value as handled in PHP
|
||||
* @return mixed The attribute value as stored in DB
|
||||
*/
|
||||
public function to_db($value) {
|
||||
return is_null($value)?$this -> default():$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value from string
|
||||
* @param string $value The input value
|
||||
* @return mixed The attribute value as handled in PHP
|
||||
*/
|
||||
public function from_string($value) {
|
||||
return self :: from_db($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value to string
|
||||
* @param mixed $value The input value as handled in PHP
|
||||
* @return string The attribute value as string
|
||||
*/
|
||||
public function to_string($value) {
|
||||
$value = self :: to_db($value);
|
||||
return is_null($value)?'':strval($value);
|
||||
}
|
||||
|
||||
}
|
49
src/Db/AttrBool.php
Normal file
49
src/Db/AttrBool.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
class AttrBool extends Attr {
|
||||
|
||||
/**
|
||||
* The value stored in database for true
|
||||
* @var mixed
|
||||
*/
|
||||
public static $true_value = 1;
|
||||
|
||||
/**
|
||||
* The value stored in database for false
|
||||
* @var mixed
|
||||
*/
|
||||
public static $false_value = 0;
|
||||
|
||||
/**
|
||||
* Compute attribute value from DB
|
||||
* @param mixed $value The value as retrieved from debug
|
||||
* @return bool|null The attribute value
|
||||
*/
|
||||
public function from_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
switch ($value) {
|
||||
case static :: $true_value:
|
||||
return true;
|
||||
case static :: $false_value:
|
||||
return false;
|
||||
case null:
|
||||
return null;
|
||||
}
|
||||
throw new DbException("Unknown value '%s' retrieved for %s value", $value, get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value for DB
|
||||
* @param mixed $value The value as handled in PHP
|
||||
* @return mixed The attribute value as stored in DB
|
||||
*/
|
||||
public function to_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
if(is_null($value))
|
||||
return null;
|
||||
return $value?static :: $true_value:static :: $false_value;
|
||||
}
|
||||
|
||||
}
|
34
src/Db/AttrInt.php
Normal file
34
src/Db/AttrInt.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
class AttrInt extends Attr {
|
||||
|
||||
/**
|
||||
* Auto-increment flag
|
||||
* Note: use to set this attribute as optional on creation.
|
||||
* @var bool
|
||||
*/
|
||||
public $autoincrement = false;
|
||||
|
||||
/**
|
||||
* Compute attribute value from DB
|
||||
* @param int|null $value The value as retrieved from debug
|
||||
* @return int|null The attribute value
|
||||
*/
|
||||
public function from_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
return is_null($value)?null:intval($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value for DB
|
||||
* @param int|null $value The value as handled in PHP
|
||||
* @return int|null The attribute value as stored in DB
|
||||
*/
|
||||
public function to_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
return is_null($value)?null:intval($value);
|
||||
}
|
||||
|
||||
}
|
27
src/Db/AttrStr.php
Normal file
27
src/Db/AttrStr.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
class AttrStr extends Attr {
|
||||
|
||||
/**
|
||||
* Compute attribute value from DB
|
||||
* @param string|null $value The value as retrieved from debug
|
||||
* @return string|null The attribute value
|
||||
*/
|
||||
public function from_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
return is_null($value)?null:strval($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value for DB
|
||||
* @param string|null $value The value as handled in PHP
|
||||
* @return string|null The attribute value as stored in DB
|
||||
*/
|
||||
public function to_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
return is_null($value)?null:strval($value);
|
||||
}
|
||||
|
||||
}
|
74
src/Db/AttrTimestamp.php
Normal file
74
src/Db/AttrTimestamp.php
Normal file
|
@ -0,0 +1,74 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
use EesyPHP\Date;
|
||||
|
||||
use function EesyPHP\get_system_timezone;
|
||||
|
||||
class AttrTimestamp extends Attr {
|
||||
|
||||
/**
|
||||
* The timezone of the date
|
||||
* @var \DateTimeZone|string|null
|
||||
*/
|
||||
protected $timezone = 'system';
|
||||
|
||||
/**
|
||||
* The export format
|
||||
* @var string
|
||||
*/
|
||||
protected $export_format = 'Y/m/d H:i:s';
|
||||
|
||||
/**
|
||||
* Compute attribute value from DB
|
||||
* @param int|null $value The value as retrieved from debug
|
||||
* @return \DateTime|null The attribute value
|
||||
*/
|
||||
public function from_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
if (is_null($value)) return null;
|
||||
return Date :: from_timestamp($value, $this -> timezone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value for DB
|
||||
* @param \DateTime|int|null $value The value as handled in PHP
|
||||
* @return int|null The attribute value as stored in DB
|
||||
*/
|
||||
public function to_db($value) {
|
||||
$value = parent::from_db($value);
|
||||
if (is_null($value)) return null;
|
||||
$value = $value instanceof \DateTime?$value:Date :: from_timestamp($value);
|
||||
return $value->getTimestamp();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value from string
|
||||
* @param string $value The input value
|
||||
* @return \DateTime|null The attribute value as handled in PHP
|
||||
*/
|
||||
public function from_string($value) {
|
||||
if (!$value) return null;
|
||||
$timestamp = Date :: parse($value, $this -> export_format, null, true);
|
||||
if ($timestamp === false)
|
||||
throw new DbException(
|
||||
"Error parsing date '%s' from export using format '%s'",
|
||||
$value, $this -> export_format
|
||||
);
|
||||
return $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute attribute value to string
|
||||
* @param \DateTime|int|null $value The input value as handled in PHP
|
||||
* @return string The attribute value as string
|
||||
*/
|
||||
public function to_string($value) {
|
||||
$value = parent::from_db($value);
|
||||
if (is_null($value)) return '';
|
||||
$value = $value instanceof \DateTime?$value:Date :: from_timestamp($value);
|
||||
return Date :: format($value->getTimestamp(), $this -> export_format);
|
||||
}
|
||||
|
||||
}
|
17
src/Db/DbException.php
Normal file
17
src/Db/DbException.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
class DbException extends \Exception {
|
||||
public function __construct($message, ...$extra_args) {
|
||||
// If extra arguments passed, format error message using sprintf
|
||||
if ($extra_args) {
|
||||
$message = call_user_func_array(
|
||||
'sprintf',
|
||||
array_merge(array($message), $extra_args)
|
||||
);
|
||||
}
|
||||
|
||||
parent::__construct($message);
|
||||
}
|
||||
}
|
582
src/Db/DbObject.php
Normal file
582
src/Db/DbObject.php
Normal file
|
@ -0,0 +1,582 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Db;
|
||||
|
||||
use EesyPHP\Hook;
|
||||
use EesyPHP\Log;
|
||||
|
||||
use function EesyPHP\implode_with_keys;
|
||||
|
||||
/**
|
||||
* @property-read array<string,Attr> $_schema
|
||||
* @property-read bool $_is_new
|
||||
*/
|
||||
class DbObject {
|
||||
|
||||
/**
|
||||
* Possible order directions
|
||||
* @var array<string>
|
||||
*/
|
||||
public const ORDER_DIRECTIONS = ['ASC', 'DESC'];
|
||||
|
||||
/**
|
||||
* Default number of objects by page
|
||||
* @var int
|
||||
*/
|
||||
public const DEFAULT_NB_BY_PAGE = 25;
|
||||
|
||||
/**
|
||||
* The database class
|
||||
* @var class-string
|
||||
*/
|
||||
protected const DB_CLASS = '\\EesyPHP\\Db';
|
||||
|
||||
/**
|
||||
* The table name where are stored these objects
|
||||
* @var string
|
||||
*/
|
||||
protected const TABLE = 'NOTSET';
|
||||
|
||||
/**
|
||||
* Primary keys (optional, default: the first field of the schema)
|
||||
* @var array<string>
|
||||
*/
|
||||
protected const PRIMARY_KEYS = [];
|
||||
|
||||
/**
|
||||
* The default order (optional, default: the first field of the schema)
|
||||
* @var string|null
|
||||
*/
|
||||
protected const DEFAULT_ORDER = null;
|
||||
|
||||
/**
|
||||
* The default order direction
|
||||
* @var value-of<DbObject::ORDER_DIRECTIONS>
|
||||
*/
|
||||
protected const DEFAULT_ORDER_DIRECTION = 'DESC';
|
||||
|
||||
/**
|
||||
* Possible orders (optional, default: all declared fields in schema)
|
||||
* @var array<string>|null
|
||||
*/
|
||||
protected const POSSIBLE_ORDERS = null;
|
||||
|
||||
/**
|
||||
* Object's properties values as stored in database
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected $_properties;
|
||||
|
||||
/**
|
||||
* Flag to known if it's a new object
|
||||
* @var bool
|
||||
*/
|
||||
protected $_is_new;
|
||||
|
||||
/**
|
||||
* Cache of the object's property _schema
|
||||
* @var array<string,Attr>|null
|
||||
*/
|
||||
protected $__schema = null;
|
||||
|
||||
/**
|
||||
* Object constructor
|
||||
* @param array<string,mixed> $properties Object's properties as stored in database
|
||||
* @param boolean $is_new Set to false if it's an existing object in database
|
||||
*/
|
||||
public function __construct($properties=null, $is_new=true) {
|
||||
$this -> _properties = is_array($properties)?$properties:[];
|
||||
foreach($this -> _schema as $prop => $attr)
|
||||
if (!array_key_exists($prop, $this -> _properties))
|
||||
$this -> _properties[$prop] = $attr->to_db(null);
|
||||
$this -> _is_new = boolval($is_new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object schema
|
||||
* @throws DbException
|
||||
* @return array<string,Attr> The object's schema
|
||||
*/
|
||||
protected static function get_schema() {
|
||||
throw new DbException("The get_schema() method is not implemented for %s", get_called_class());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object's primary keys
|
||||
* @param array<string,Attr>|null $schema The object's schema (optional)
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function primary_keys($schema=null) {
|
||||
if (static :: PRIMARY_KEYS)
|
||||
return static :: PRIMARY_KEYS;
|
||||
$schema = $schema ? $schema : static :: get_schema();
|
||||
return [key($schema)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get object's primary keys value
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
public function get_primary_keys($with_value=false) {
|
||||
$pks = [];
|
||||
foreach(static :: primary_keys($this -> _schema) as $pk)
|
||||
$pks[$pk] = $this -> $pk;
|
||||
return $pks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WHERE clause from object primary keys
|
||||
* @throws DbException
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
protected function get_primary_keys_where_clauses() {
|
||||
$where = [];
|
||||
foreach(static :: get_primary_keys($this -> _schema) as $prop => $value) {
|
||||
if (is_null($value))
|
||||
throw new DbException(
|
||||
"No value for primary key %s of %s object",
|
||||
$prop, get_called_class()
|
||||
);
|
||||
$where[$prop] = $this -> _schema[$prop] -> to_db($value);
|
||||
}
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default order
|
||||
* @param array<string,Attr>|null $schema The object's schema (optional)
|
||||
* @return string
|
||||
*/
|
||||
public static function default_order($schema=null) {
|
||||
if (static :: DEFAULT_ORDER)
|
||||
return static :: DEFAULT_ORDER;
|
||||
$schema = $schema ? $schema : static :: get_schema();
|
||||
return key($schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default order direction
|
||||
* @return value-of<DbObject::ORDER_DIRECTIONS>
|
||||
*/
|
||||
public static function default_order_direction() {
|
||||
return static :: DEFAULT_ORDER_DIRECTION;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get possible orders
|
||||
* @return array<string>
|
||||
*/
|
||||
public static function possible_orders() {
|
||||
if (static :: POSSIBLE_ORDERS)
|
||||
return static :: POSSIBLE_ORDERS;
|
||||
$schema = static :: get_schema();
|
||||
return array_keys($schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object's property
|
||||
* @param string $prop The property name
|
||||
* @throws DbException
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($prop) {
|
||||
switch ($prop) {
|
||||
case '_schema':
|
||||
if (is_null($this -> __schema))
|
||||
$this -> __schema = static :: get_schema();
|
||||
return $this -> __schema;
|
||||
case '_is_new':
|
||||
return $this -> _is_new;
|
||||
}
|
||||
if (!array_key_exists($prop, $this -> _schema))
|
||||
throw new DbException("%s object as no %s field", get_called_class(), $prop);
|
||||
return $this -> _schema[$prop] -> from_db(
|
||||
array_key_exists($prop, $this -> _properties)?
|
||||
$this -> _properties[$prop]:
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an object's property
|
||||
* @param string $prop The property name
|
||||
* @throws DbException
|
||||
* @param mixed $value The property value
|
||||
*/
|
||||
public function __set($prop, $value) {
|
||||
if (!array_key_exists($prop, $this -> _schema))
|
||||
throw new DbException("%s object as no %s field", get_called_class(), $prop);
|
||||
$this -> _properties[$prop] = $this -> _schema[$prop] -> to_db($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to apply changes pass as an associative array on the object
|
||||
* @param array<string,mixed> $changes The changes to apply
|
||||
* @throws DbException
|
||||
* @return void
|
||||
*/
|
||||
public function apply($changes) {
|
||||
foreach($changes as $attr => $value)
|
||||
$this -> $attr = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get one object from its primary keys
|
||||
* @param array<mixed> $args Primary keys value in the same order as primary keys as declared
|
||||
* @see static :: PRIMARY_KEYS
|
||||
* @throws DbException
|
||||
* @return DbObject|null The object if found, else otherwise.
|
||||
*/
|
||||
public static function get(...$args) {
|
||||
$class = get_called_class();
|
||||
$schema = static :: get_schema();
|
||||
$primary_keys = static :: primary_keys($schema);
|
||||
$where = [];
|
||||
foreach($primary_keys as $prop) {
|
||||
if (empty($args))
|
||||
throw new DbException("No value provide for %s", $prop);
|
||||
$value = array_shift($args);
|
||||
$where[$prop] = $schema[$prop] -> to_db($value);
|
||||
}
|
||||
|
||||
if (!empty($args))
|
||||
throw new DbException(
|
||||
"Too much arguments provided to get one %s. Expect the following fields: %s",
|
||||
$class,
|
||||
implode(', ', $primary_keys)
|
||||
);
|
||||
|
||||
$properties = static :: DB_CLASS :: get_one(
|
||||
static :: TABLE,
|
||||
$where,
|
||||
array_keys($schema)
|
||||
);
|
||||
if ($properties === false)
|
||||
return null;
|
||||
|
||||
return new $class($properties, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save changes on object
|
||||
* @return boolean
|
||||
*/
|
||||
public function save() {
|
||||
Hook :: trigger(get_called_class().'::'.($this -> _is_new?'adding':'updating'), $this);
|
||||
foreach($this -> _schema as $prop => $info) {
|
||||
if (!$info->required || !is_null($this->$prop)) continue;
|
||||
if ($this -> _is_new && property_exists($info, 'autoincrement') && $info->autoincrement)
|
||||
continue;
|
||||
Log :: warning(
|
||||
"%s->save(): required property %s is missing, can't save it.",
|
||||
get_called_class(), $prop
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if ($this -> _is_new) {
|
||||
Log :: debug("%s->save(): object marked as new, use INSERT query", get_called_class());
|
||||
$values = [];
|
||||
foreach($this -> _properties as $prop => $value)
|
||||
if (!is_null($value))
|
||||
$values[$prop] = $value;
|
||||
$result = static :: DB_CLASS :: insert(static :: TABLE, $values, true);
|
||||
if ($result === false)
|
||||
return false;
|
||||
|
||||
// Store object ID if object as an unique integer primary key
|
||||
if (is_int($result)) {
|
||||
$pks = $this -> primary_keys($this -> _schema);
|
||||
if (count($pks) == 1 && $this -> _schema[$this -> $pks[0]] instanceof AttrInt)
|
||||
$this -> $pks[0] = $result;
|
||||
}
|
||||
Log :: info(
|
||||
"New %s added (%s)",
|
||||
get_called_class(),
|
||||
implode_with_keys($this -> get_primary_keys())
|
||||
);
|
||||
|
||||
Hook :: trigger(get_called_class().'::added', $this);
|
||||
return true;
|
||||
}
|
||||
|
||||
Log :: debug("%s->save(): object not marked as new, use UPDATE query", get_called_class());
|
||||
if (static :: DB_CLASS :: update(
|
||||
static :: TABLE,
|
||||
$this -> _properties,
|
||||
$this -> get_primary_keys_where_clauses()
|
||||
)) {
|
||||
Log :: info(
|
||||
"%s %s updated",
|
||||
get_called_class(),
|
||||
implode_with_keys($this -> get_primary_keys())
|
||||
);
|
||||
Hook :: trigger(get_called_class().'::updated', $this);
|
||||
return True;
|
||||
}
|
||||
Log :: error(
|
||||
"Error saving changes in %s %s.",
|
||||
get_called_class(),
|
||||
implode_with_keys($this -> get_primary_keys())
|
||||
);
|
||||
return False;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the object
|
||||
* @return boolean
|
||||
*/
|
||||
public function delete() {
|
||||
Hook :: trigger(get_called_class().'::deleting', $this);
|
||||
if (!static :: DB_CLASS :: delete(
|
||||
static :: TABLE,
|
||||
$this -> get_primary_keys_where_clauses()
|
||||
))
|
||||
return False;
|
||||
$this -> _is_new = true;
|
||||
Hook :: trigger(get_called_class().'::deleted', $this);
|
||||
return True;
|
||||
}
|
||||
|
||||
/**
|
||||
* List objects
|
||||
* @param array<string,mixed> $where Where clauses as associative array of field name and value
|
||||
* @return array<DbObject>|false
|
||||
*/
|
||||
public static function list($where=null) {
|
||||
$class = get_called_class();
|
||||
$schema = static :: get_schema();
|
||||
$rows = static :: DB_CLASS :: get_many(
|
||||
static :: TABLE,
|
||||
static :: _compute_where_clauses($where),
|
||||
array_keys($schema)
|
||||
);
|
||||
if (!is_array($rows))
|
||||
return false;
|
||||
$objects = [];
|
||||
foreach($rows as $row)
|
||||
$objects[] = new $class($row, false);
|
||||
return $objects;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute WHERE clauses
|
||||
* @param array<string,mixed>|null $where Where clauses as associative array of field name and value
|
||||
* @param array<string,Attr>|null $schema The object's schema (optional)
|
||||
* @throws DbException
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
protected static function _compute_where_clauses($where, $schema=null) {
|
||||
if (is_null($where)) return [];
|
||||
$schema = $schema?$schema:static :: get_schema();
|
||||
foreach(array_keys($where) as $prop) {
|
||||
if (!array_key_exists($prop, $schema))
|
||||
throw new DbException("%s object as no %s field", get_called_class(), $prop);
|
||||
$where[$prop] = $schema[$prop] -> to_db($where[$prop]);
|
||||
}
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute WHERE clauses from word pattern
|
||||
* @throws DbException
|
||||
* @return array
|
||||
*/
|
||||
public static function word_to_filters($word) {
|
||||
throw new DbException(
|
||||
"The word_to_filters() method is not implemented for %s",
|
||||
get_called_class()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search objects
|
||||
* @param array<string,mixed> $params Search parameters as an associative array
|
||||
* [
|
||||
* 'pattern' => [pattern],
|
||||
* 'filters' => ['field1' => 'value1', ...],
|
||||
* 'where' => [WHERE clauses as expected by FPdo (aggregate with a AND logical operator)],
|
||||
* 'order' => [order by field],
|
||||
* 'order_direction' => [ordering direction],
|
||||
* 'all' => bool,
|
||||
* 'offset' => int,
|
||||
* 'limit' => int,
|
||||
* // or:
|
||||
* 'page' => integer, // default: 1
|
||||
* 'nb_by_page' => integer, // default: static :: DEFAULT_NB_BY_PAGE
|
||||
* ]
|
||||
* @throws DbException
|
||||
* @return array|false The search result as an array, or False in case of error.
|
||||
* Search result format:
|
||||
* [
|
||||
* 'first' => [range of the first item in result],
|
||||
* 'last' => [range of the last item in result],
|
||||
* 'count' => [total number of items return by the search],
|
||||
* 'items' => [array of DbObjects],
|
||||
* // paged search (simulate in non-paged search):
|
||||
* 'page' => [page starting by 1],
|
||||
* 'nb_pages' => [number of pages],
|
||||
* // non-paged search:
|
||||
* 'offset' => [the offset of the search],
|
||||
* 'limit' => [the limit of the search],
|
||||
* ]
|
||||
*/
|
||||
public static function search($params) {
|
||||
$class = get_called_class();
|
||||
$schema = static :: get_schema();
|
||||
|
||||
$where = isset($params['where'])?$params['where']:[];
|
||||
if (isset($params['filters']))
|
||||
$where = array_merge(static :: _compute_where_clauses($params['filters']), $where);
|
||||
|
||||
$patterns_where = array();
|
||||
if (isset($params['pattern']) && $params['pattern']) {
|
||||
foreach(preg_split('/\s+/', trim($params['pattern'])) as $word) {
|
||||
if (!$word) continue;
|
||||
$patterns_where[] = static :: word_to_filters($word);
|
||||
}
|
||||
}
|
||||
|
||||
$order = static :: default_order();
|
||||
if (isset($params['order'])) {
|
||||
$orders = static :: possible_orders();
|
||||
if (!in_array($order, $orders))
|
||||
throw new DbException(
|
||||
"Invalid order '%s' clause for objects %s",
|
||||
$params['order'], $class
|
||||
);
|
||||
$order = $params['order'];
|
||||
}
|
||||
|
||||
$order_direction = static :: default_order_direction();
|
||||
if (isset($params['order_direction']) && $params['order_direction']) {
|
||||
if (!in_array($params['order_direction'], self :: ORDER_DIRECTIONS))
|
||||
throw new DbException(
|
||||
"Invalid order direction '%s' clause for objects %s",
|
||||
$params['order_direction'], $class
|
||||
);
|
||||
$order_direction=$params['order_direction'];
|
||||
}
|
||||
|
||||
$orderby = "$order $order_direction";
|
||||
|
||||
$limit = null;
|
||||
$offset = 0;
|
||||
$page = 1;
|
||||
$nb_by_page = static :: DEFAULT_NB_BY_PAGE;
|
||||
if (isset($params['all'])) {
|
||||
if (isset($params['limit']))
|
||||
$limit = intval($params['limit']);
|
||||
if (isset($params['offset']))
|
||||
$offset = intval($params['offset']);
|
||||
}
|
||||
else {
|
||||
if (isset($params['page']) && $params['page'] > 0) {
|
||||
if (isset($params['nb_by_page']) && $params['nb_by_page'] > 0) {
|
||||
$nb_by_page = intval($params['nb_by_page']);
|
||||
}
|
||||
$page = intval($params['page']);
|
||||
}
|
||||
$offset = ($page - 1) * $nb_by_page;
|
||||
$limit = $nb_by_page;
|
||||
}
|
||||
|
||||
try {
|
||||
$query = static :: DB_CLASS :: $fpdo -> from(static :: TABLE);
|
||||
if (!empty($where))
|
||||
$query -> where($where);
|
||||
foreach ($patterns_where as $patterns_word)
|
||||
call_user_func_array(
|
||||
array($query, 'where'),
|
||||
array_merge(
|
||||
array('('.implode(' OR ', array_keys($patterns_word)).')'),
|
||||
array(array_values($patterns_word))
|
||||
)
|
||||
);
|
||||
$result = $query -> orderBy($orderby)
|
||||
-> limit($limit)
|
||||
-> offset($offset)
|
||||
-> execute();
|
||||
|
||||
if ($result === false) {
|
||||
Log :: error('%s :: search() : search in DB return false', $class);
|
||||
return false;
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($result -> fetchAll() as $row)
|
||||
$items[] = new $class($row, false);
|
||||
|
||||
if (isset($params['all'])) {
|
||||
return array(
|
||||
'count' => count($items),
|
||||
'first' => 1,
|
||||
'last' => count($items),
|
||||
'nb_pages' => 1,
|
||||
'page' => 1,
|
||||
'offset' => $offset,
|
||||
'limit' => $limit,
|
||||
'items' => $items
|
||||
);
|
||||
}
|
||||
$query_count = static :: DB_CLASS :: $fpdo -> from('item')
|
||||
-> select(null)
|
||||
-> select('COUNT(*) as count');
|
||||
if (!empty($where))
|
||||
$query_count -> where($where);
|
||||
foreach ($patterns_where as $patterns_word)
|
||||
call_user_func_array(
|
||||
array($query_count, 'where'),
|
||||
array_merge(
|
||||
array('('.implode(' OR ', array_keys($patterns_word)).')'),
|
||||
array(array_values($patterns_word))
|
||||
)
|
||||
);
|
||||
|
||||
$result_count = $query_count -> execute();
|
||||
|
||||
if ($result_count === false) {
|
||||
Log :: debug('%s :: search() : search for count in DB return false', $class);
|
||||
return False;
|
||||
}
|
||||
$count = $result_count -> fetch();
|
||||
return array(
|
||||
'count' => $count['count'],
|
||||
'first' => $offset+1,
|
||||
'last' => (
|
||||
$offset + $nb_by_page < $count['count']?
|
||||
$offset + $nb_by_page:
|
||||
$count['count']
|
||||
),
|
||||
'nb_pages' => ceil($count['count']/$nb_by_page),
|
||||
'page' => $page,
|
||||
'items' => $items,
|
||||
);
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
Log :: exception(
|
||||
$e, "An exception occurred searching %s with params %s in database",
|
||||
get_called_class(),
|
||||
preg_replace("/\n[ \t]*/", " ", print_r($params, true))
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute CSV export fields as expected by \EesyPHP\Export\CSV
|
||||
* @param array<string,Attr>|null $schema The object's schema (optional)
|
||||
* @return array
|
||||
*/
|
||||
static protected function csv_export_fields($schema=null) {
|
||||
$schema = $schema?$schema:static :: get_schema();
|
||||
$csv_fields = [];
|
||||
foreach($schema as $field => $attr)
|
||||
$csv_fields[$field] = [
|
||||
'label' => $field,
|
||||
'from_string' => [$attr, 'from_string'],
|
||||
'to_string' => [$attr, 'to_string'],
|
||||
];
|
||||
return $csv_fields;
|
||||
}
|
||||
|
||||
}
|
240
src/Export/CSV.php
Normal file
240
src/Export/CSV.php
Normal file
|
@ -0,0 +1,240 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Export;
|
||||
|
||||
use EesyPHP\Date;
|
||||
use EesyPHP\Log;
|
||||
|
||||
use function EesyPHP\implode_with_keys;
|
||||
|
||||
/**
|
||||
* CSV export
|
||||
* @property-read string $separator
|
||||
* @property-read string $enclosure
|
||||
* @property-read string $escape
|
||||
* @property-read string $eol
|
||||
* @property-read int|null $max_length
|
||||
* @property-read string $date_format
|
||||
* @property-read bool|string $trim
|
||||
*/
|
||||
class CSV extends Generic {
|
||||
|
||||
/**
|
||||
* Array of fields in export. Could be an associative array to specify custom exporting
|
||||
* parameters:
|
||||
* [
|
||||
* 'name',
|
||||
* 'name' => 'label',
|
||||
* 'name' => [
|
||||
* 'label' => 'Name',
|
||||
* 'to_string' => [callable],
|
||||
* 'from_string' => [callable],
|
||||
* ],
|
||||
* ].
|
||||
* @var array<string,string>|array<string>
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* Array of export parameters default value
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected static $default_parameters = [
|
||||
"separator" => ",", // The fieds separator character
|
||||
"enclosure" => "\"", // The enclosure character
|
||||
"escape" => "\\", // The enclosure character
|
||||
"eol" => "\n", // The end-of-line character. Note: only used in PHP >= 8.1.0.
|
||||
// Line max length, see fgetcsv()
|
||||
"max_length" => PHP_VERSION_ID >= 80000?null:99999,
|
||||
// DateTime object exporting format
|
||||
"date_format" => 'Y/m/d H:i:s',
|
||||
// Specify if values loaded from CSV file have to be trim. Could be a boolean or a string of the
|
||||
// stripped characters.
|
||||
"trim" => true,
|
||||
];
|
||||
|
||||
/**
|
||||
* Compute fields mapping info
|
||||
* @return array<string,mixed> Fields mapping info
|
||||
*/
|
||||
protected function _fields_mapping() {
|
||||
$map = [];
|
||||
foreach(parent :: _fields_mapping() as $key => $info) {
|
||||
$map[$key] = [
|
||||
'label' => $info['label'],
|
||||
'to_string' => (
|
||||
array_key_exists('to_string', $info)?
|
||||
$info['to_string']:[$this, 'to_string']
|
||||
),
|
||||
'from_string' => (
|
||||
array_key_exists('from_string', $info)?
|
||||
$info['from_string']:[$this, 'from_string']
|
||||
),
|
||||
];
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert value to string as expected in the export
|
||||
* @param mixed $value The value to export
|
||||
* @return string The value as string
|
||||
*/
|
||||
protected function to_string($value) {
|
||||
if (is_null($value))
|
||||
return '';
|
||||
if (is_a($value, "\DateTime"))
|
||||
return Date :: format($value, $this -> date_format);
|
||||
return strval($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert value from string
|
||||
* @param string $value The value to convert
|
||||
* @return mixed The converted value
|
||||
*/
|
||||
protected function from_string($value) {
|
||||
if ($this -> trim) {
|
||||
if (is_string($this -> trim))
|
||||
$value = trim($value, $this -> trim);
|
||||
else
|
||||
$value = trim($value);
|
||||
}
|
||||
return empty($value)?null:$value;
|
||||
}
|
||||
|
||||
/**
|
||||
* fputcsv wrapper in context of this export
|
||||
* @param resource $fd The file pointer of the export
|
||||
* @param array<string> $fields The field of the row to export
|
||||
* @return boolean
|
||||
*/
|
||||
public function fputcsv($fd, $fields) {
|
||||
$args = [$fd, $fields, $this -> separator, $this -> enclosure, $this -> escape];
|
||||
if (PHP_VERSION_ID >= 80100)
|
||||
$args[] = $this -> eol;
|
||||
return call_user_func_array('fputcsv', $args) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* fgetcsv wrapper in context of this export
|
||||
* @param resource $fd The file pointer of the import
|
||||
* @return array<string>|false The retrieved row or false in case of error or at the end of the
|
||||
* file
|
||||
*/
|
||||
public function fgetcsv($fd) {
|
||||
return fgetcsv(
|
||||
$fd,
|
||||
$this -> max_length,
|
||||
$this -> separator,
|
||||
$this -> enclosure,
|
||||
$this -> escape
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export items
|
||||
* @param array<array<string,mixed>> $items The items to export
|
||||
* @param resource|null $fd The file pointer where to export (optional, default: php://output)
|
||||
* @return boolean
|
||||
*/
|
||||
public function export($items, $fd=null) {
|
||||
if (!$fd) $fd = fopen('php://output', 'w');
|
||||
$mapping = $this -> _fields_mapping();
|
||||
$headers = [];
|
||||
foreach ($mapping as $field)
|
||||
$headers[] = $field['label'];
|
||||
$success = $this -> fputcsv($fd, array_values($headers));
|
||||
foreach($items as $item) {
|
||||
$row = [];
|
||||
foreach ($mapping as $key => $info) {
|
||||
$row[] = call_user_func(
|
||||
$info['to_string'],
|
||||
array_key_exists($key, $item)?$item[$key]:null
|
||||
);
|
||||
}
|
||||
$success = $success && $this -> fputcsv($fd, $row);
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load items
|
||||
* @param resource $fd The file pointer where to load data
|
||||
* @return array<int,array<string,mixed>>|false The loaded items or false in case of error
|
||||
*/
|
||||
public function load($fd=null) {
|
||||
if (!$fd) $fd = fopen('php://stdin', 'r');
|
||||
$mapping = $this -> _fields_mapping();
|
||||
$rows_mapping = false;
|
||||
$line = 0;
|
||||
$items = [];
|
||||
$error = false;
|
||||
while (($row = $this -> fgetcsv($fd)) !== FALSE) {
|
||||
$line++;
|
||||
if ($rows_mapping === false) {
|
||||
$rows_mapping = [];
|
||||
foreach($row as $idx => $field) {
|
||||
$map = false;
|
||||
foreach($mapping as $map_field => $map_info) {
|
||||
if ($map_info['label'] == $field) {
|
||||
$map = $map_field;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($map) {
|
||||
$rows_mapping[$idx] = $mapping[$map];
|
||||
$rows_mapping[$idx]['name'] = $map;
|
||||
unset($mapping[$map]);
|
||||
continue;
|
||||
}
|
||||
Log :: warning(
|
||||
"No corresponding field found for column '%s' (#%d), ignore it.",
|
||||
$field, $idx
|
||||
);
|
||||
}
|
||||
if (!$rows_mapping) {
|
||||
Log :: warning("Invalid rows mapping loaded from line #%d", $line);
|
||||
return false;
|
||||
}
|
||||
Log :: debug(
|
||||
"CSV :: load(): Rows mapping established from row #%d : %s",
|
||||
$line,
|
||||
implode(
|
||||
', ',
|
||||
array_map(
|
||||
function($idx, $info) {
|
||||
return sprintf("%s => %s", $idx, $info["label"]);
|
||||
},
|
||||
array_keys($rows_mapping),
|
||||
array_values($rows_mapping)
|
||||
)
|
||||
)
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$item = [];
|
||||
foreach($rows_mapping as $idx => $field) {
|
||||
$item[$field['name']] = call_user_func(
|
||||
$field['from_string'],
|
||||
$row[$idx]
|
||||
);
|
||||
}
|
||||
Log :: trace("CSV :: load(): Item load from line #%d: %s", $line, implode_with_keys($item));
|
||||
$items[] = $item;
|
||||
}
|
||||
catch (\Exception $e) {
|
||||
Log :: error(
|
||||
"Error occurred loading item from line #%d : %s\n%s",
|
||||
$line,
|
||||
$e->getMessage(),
|
||||
print_r($row, true)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Log :: debug("CSV :: load(): %d item(s) loaded", count($items));
|
||||
return $items;
|
||||
}
|
||||
}
|
98
src/Export/Generic.php
Normal file
98
src/Export/Generic.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace EesyPHP\Export;
|
||||
|
||||
class Generic {
|
||||
|
||||
/**
|
||||
* Array of fields in export. Could be an associative array to specify custom exporting
|
||||
* parameters:
|
||||
* [
|
||||
* 'name',
|
||||
* 'name' => 'label',
|
||||
* 'name' => [
|
||||
* 'label' => 'Name',
|
||||
* // all other export specific stuff
|
||||
* ],
|
||||
* ].
|
||||
* @var array<string,array>|array<string,string>|array<string>
|
||||
*/
|
||||
protected $fields;
|
||||
|
||||
/**
|
||||
* Array of export parameters
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected $parameters;
|
||||
|
||||
/**
|
||||
* Array of export parameters default value
|
||||
* @var array<string,mixed>
|
||||
*/
|
||||
protected static $default_parameters = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param array<string,string>|array<string> $fields Array of the fields name in export
|
||||
* @param array<string,mixed> $parameters Export parameters
|
||||
*/
|
||||
|
||||
public function __construct($fields, $parameters=null) {
|
||||
$this -> fields = $fields;
|
||||
$this -> parameters = is_array($parameters)?$parameters:[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parameter
|
||||
* @param string $param Parameter name
|
||||
* @param mixed $default Override parameter default value (optional)
|
||||
* @return mixed Parameter value
|
||||
*/
|
||||
public function get_parameter($param, $default=null) {
|
||||
$default = (
|
||||
$default?
|
||||
$default:
|
||||
(
|
||||
array_key_exists($param, static :: $default_parameters)?
|
||||
static :: $default_parameters[$param]:
|
||||
null
|
||||
)
|
||||
);
|
||||
return (
|
||||
array_key_exists($param, $this -> parameters)?
|
||||
$this -> parameters[$param]:
|
||||
$default
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow to accept parameters as properties
|
||||
* @param string $key
|
||||
* @return mixed
|
||||
*/
|
||||
public function __get($key) {
|
||||
return $this -> get_parameter($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute fields mapping info
|
||||
* @return array<string,mixed> Fields mapping info
|
||||
*/
|
||||
protected function _fields_mapping() {
|
||||
$map = [];
|
||||
foreach($this -> fields as $key => $value) {
|
||||
$key = is_int($key)?$value:$key;
|
||||
$map[$key] = [
|
||||
'label' => (
|
||||
is_array($value) && array_key_exists('label', $value)?
|
||||
$value['label']:strval($value)
|
||||
),
|
||||
];
|
||||
// Keep all other import generic provided info and leave specific export type to handle them
|
||||
if (is_array($value))
|
||||
$map[$key] = array_merge($value, $map[$key]);
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
}
|
|
@ -370,4 +370,17 @@ function generate_uuid() {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get system timezone
|
||||
* @param bool $as_string Set to true to retrieve timezone name instead of a DateTimeZone object
|
||||
* @return ($as_string is true ? string : \DateTimeZone) System timezone
|
||||
*/
|
||||
function get_system_timezone($as_string=false) {
|
||||
$timezone = trim(
|
||||
$_SERVER['TZ'] ??
|
||||
(file_get_contents('/etc/timezone') ?: file_get_contents('/etc/localtime'))
|
||||
);
|
||||
return $as_string?$timezone:new \DateTimeZone($timezone);
|
||||
}
|
||||
|
||||
# vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab
|
||||
|
|
Loading…
Reference in a new issue