Release of v4.0.1-alpha6

Add new subform classes. Fix registry class methods return type. Update all list and custom fields to use the new layouts.
This commit is contained in:
2024-06-28 03:50:30 +02:00
parent 5a2e137f31
commit 017d6a6299
92 changed files with 1692 additions and 469 deletions

View File

@@ -112,6 +112,33 @@ class Load implements LoadInterface
);
}
/**
* Get a value from multiple rows from a given table
* Example: $this->values(
* [
* 'guid' => 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
* ], 'value_key'
* );
*
* @param array $keys The item keys
* @param string $field The field key
*
* @return array|null
* @since 3.2.2
*/
public function values(array $keys, string $field): ?array
{
return $this->model->values(
$this->load->values(
["a.{$field}" => $field],
['a' => $this->getTable()],
$this->prefix($keys)
),
$field,
$this->getTable()
);
}
/**
* Get values from a given table
* Example: $this->item(

View File

@@ -0,0 +1,46 @@
<?php
/**
* @package Joomla.Component.Builder
*
* @created 4th September, 2022
* @author Llewellyn van der Merwe <https://dev.vdm.io>
* @git Joomla Component Builder <https://git.vdm.dev/joomla/Component-Builder>
* @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace VDM\Joomla\Data;
use Joomla\DI\Container;
use VDM\Joomla\Service\Table;
use VDM\Joomla\Service\Database;
use VDM\Joomla\Service\Model;
use VDM\Joomla\Service\Data;
use VDM\Joomla\Interfaces\FactoryInterface;
use VDM\Joomla\Abstraction\Factory as ExtendingFactory;
/**
* Data Factory
*
* @since 3.2.2
*/
abstract class Factory extends ExtendingFactory implements FactoryInterface
{
/**
* Create a container object
*
* @return Container
* @since 3.2.2
*/
protected static function createContainer(): Container
{
return (new Container())
->registerServiceProvider(new Table())
->registerServiceProvider(new Database())
->registerServiceProvider(new Model())
->registerServiceProvider(new Data());
}
}

View File

@@ -142,22 +142,7 @@ final class Item implements ItemInterface
*/
public function value(string $value, string $key = 'guid', string $get = 'id')
{
// Perform the database query
$value = $this->database->value(
["a.$get" => $get],
["a" => $this->getTable()],
["a.$key" => $value]
);
// Check if rows are found
if ($value !== null)
{
// Return the value
return $value;
}
// Return null if no rows are found
return null;
return $this->load->table($this->getTable())->value([$key => $value], $get);
}
/**
@@ -243,7 +228,13 @@ final class Item implements ItemInterface
*/
private function action(string $value, string $key): string
{
if (($id = $this->value($value, $key, 'id')) !== null && $id > 0)
$id = $this->database->value(
["a.id" => 'id'],
["a" => $this->getTable()],
["a.$key" => $value]
);
if ($id !== null && $id > 0)
{
return 'update';
}

View File

@@ -142,32 +142,18 @@ final class Items implements ItemsInterface
* @param string $key The key on which the values being searched.
* @param string $get The key of the values we want back
*
* @return array|null The array of found values.
* @return array|null The array of found values.
* @since 3.2.2
*/
public function values(array $values, string $key = 'guid', string $get = 'id'): ?array
{
// Perform the database query
$rows = $this->database->rows(
["a.$get" => $get],
["a" => $this->getTable()],
["a.$key" => ['operator' => 'IN', 'value' => $values]]
);
// Check if rows are found
if ($rows !== null)
{
// Return the values from the found rows
return array_values(
array_map(
fn($row) => $row[$get],
$rows
)
);
}
// Return null if no rows are found
return null;
return $this->load->table($this->getTable())->values([
$key => [
'operator' => 'IN',
'value' => array_values($values)
]
], $get);
}
/**
@@ -202,7 +188,7 @@ final class Items implements ItemsInterface
* @return bool
* @since 3.2.2
*/
public function delete(string $values, string $key = 'guid'): bool
public function delete(array $values, string $key = 'guid'): bool
{
return $this->delete->table($this->getTable())->items([$key => ['operator' => 'IN', 'value' => $values]]);
}
@@ -269,7 +255,12 @@ final class Items implements ItemsInterface
];
// Check for existing items.
$existingItems = $this->values($values, $key, $key);
$existingItems = $this->database->values(
["a.$key" => $key],
["a" => $this->getTable()],
["a.$key" => ['operator' => 'IN', 'value' => $values]]
);
if ($existingItems !== null)
{
$sets['update'] = $this->extractSet($items, $existingItems, $key) ?? [];

View File

@@ -0,0 +1,511 @@
<?php
/**
* @package Joomla.Component.Builder
*
* @created 3rd September, 2020
* @author Llewellyn van der Merwe <https://dev.vdm.io>
* @git Joomla Component Builder <https://git.vdm.dev/joomla/Component-Builder>
* @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace VDM\Joomla\Data;
use VDM\Joomla\Interfaces\Data\SubformInterface as Subform;
use VDM\Joomla\Interfaces\Data\MultiSubformInterface;
/**
* CRUD the data of multi subform to another views (tables)
*
* @since 3.2.2
*/
final class MultiSubform implements MultiSubformInterface
{
/**
* The Subform Class.
*
* @var Subform
* @since 3.2.2
*/
protected Subform $subform;
/**
* Constructor.
*
* @param Subform $subform The Subform Class.
*
* @since 3.2.2
*/
public function __construct(Subform $subform)
{
$this->subform = $subform;
}
/**
* Get a subform items
*
* @param array $getMap The the map to get the subfrom data
*
* Example:
* $getMap = [
* '_core' => [
* 'table' =>'data',
* 'linkValue' => $item->guid ?? '',
* 'linkKey' => 'look',
* 'field' => 'data',
* 'get' => ['guid','email','image','mobile_phone','website','dateofbirth']
* ],
* 'countries' => [
* 'table' =>'data_country',
* 'linkValue' => 'data:guid', // coretable:fieldname
* 'linkKey' => 'data',
* 'get' => ['guid','country','currency']
* ]
* ];
*
* @return array|null The subform
* @since 3.2.2
*/
public function get(array $getMap): ?array
{
// Validate the core map presence and structure
if (!isset($getMap['_core']) || !is_array($getMap['_core']) || !$this->validGetMap($getMap['_core']))
{
return null;
}
// Initialize the core data
$coreData = $this->getSubformData($getMap['_core']);
// Return null if fetching core data fails
if (null === $coreData)
{
return null;
}
$table = $getMap['_core']['table'];
unset($getMap['_core']);
// Recursively get data for all nested subforms
return $this->getNestedSubforms($getMap, $coreData, $table);
}
/**
* Set a subform items
*
* @param array $items The list of items from the subform to set
* @param array $setMap The the map to set the subfrom data
*
* Example:
* $items,
* $setMap = [
* '_core' => [
* 'table' =>'data',
* 'indexKey' => 'guid',
* 'linkKey' => 'look',
* 'linkValue' => $data['guid'] ?? ''
* ],
* 'countries' => [
* 'table' =>'data_country',
* 'indexKey' => 'guid',
* 'linkKey' => 'data',
* 'linkValue' => 'data:guid' // coretable:fieldname
* ]
* ];
*
* @return bool
* @since 3.2.2
*/
public function set(array $items, array $setMap): bool
{
// Validate the core map presence and structure
if (!isset($setMap['_core']) || !is_array($setMap['_core']) || !$this->validSetMap($setMap['_core']))
{
return false;
}
// Save the core data
if (!$this->setSubformData($items, $setMap['_core']))
{
return false;
}
$table = $setMap['_core']['table'];
unset($setMap['_core']);
// Recursively set data for all nested subforms
return $this->setNestedSubforms($setMap, $items, $table);
}
/**
* Fetch data based on provided map configuration.
*
* @param array $map Map configuration
* @param array|null $coreData The core data to be appended with subform data
*
* @return array|null Fetched data or null on failure
* @since 3.2.2
*/
private function getSubformData(array $map, ?array $coreData = null): ?array
{
$map['linkValue'] = $this->setLinkValue($map['linkValue'], $coreData);
if (empty($map['linkValue']) || strpos($map['linkValue'], ':') !== false)
{
return null;
}
return $this->subform->table($map['table'])->get(
$map['linkValue'],
$map['linkKey'],
$map['field'],
$map['get']
);
}
/**
* Set data based on provided map configuration.
*
* @param array $items The list of items from the subform to set
* @param array $map The the map to set the subfrom data
* @param array|null $coreData The core data to be appended with subform data
*
* @return bool
* @since 3.2.2
*/
private function setSubformData(array $items, array $map, ?array $coreData = null): bool
{
$map['linkValue'] = $this->setLinkValue($map['linkValue'], $coreData);
if (empty($map['linkValue']) || strpos($map['linkValue'], ':') !== false)
{
return false;
}
return $this->subform->table($map['table'])->set(
$items,
$map['indexKey'],
$map['linkKey'],
$map['linkValue']
);
}
/**
* Set the linked value if needed, and posible.
*
* @param string $linkValue The current linkValue
* @param array|null $data The already found data as table => dataSet[field] => value
*
* @return string|null The actual linkValue
* @since 3.2.2
*/
private function setLinkValue(string $linkValue, ?array $data = null): ?string
{
if ($data !== null && strpos($linkValue, ':') !== false)
{
[$table, $field] = explode(':', $linkValue);
$linkValue = $data[$table][$field] ?? null;
}
return $linkValue;
}
/**
* Recursively process additional subform data.
*
* @param array $getMap The nested map of data to process
* @param array $subformData The core subform data
* @param string $table The core table
*
* @return array The core data with nested subforms included
* @since 3.2.2
*/
private function getNestedSubforms(array $getMap, array $subformData, string $table): array
{
foreach ($subformData as &$subform)
{
$subform = $this->processGetSubform($getMap, $subform, $table);
}
return $subformData;
}
/**
* Recursively process additional subform data.
*
* @param array $setMap The nested map of data to process
* @param array $subformData The core subform data
* @param string $table The core table
*
* @return bool
* @since 3.2.2
*/
private function setNestedSubforms(array $setMap, array $subformData, string $table): bool
{
$status = true;
foreach ($subformData as $subform)
{
if (!$this->processSetSubform($setMap, $subform, $table))
{
$status = false;
}
}
return $status;
}
/**
* Process each subform entry based on the map.
*
* @param array $getMap Mapping data for processing subforms
* @param array $subform A single subform entry
* @param string $table The table name used for linking values
*
* @return array Updated subform
* @since 3.2.2
*/
private function processGetSubform(array $getMap, array $subform, string $table): array
{
foreach ($getMap as $key => $map)
{
if (!is_array($map) || isset($subform[$key]))
{
continue;
}
$this->processGetMap($subform, $map, $key, $table);
}
return $subform;
}
/**
* Process each subform entry based on the map.
*
* @param array $setMap Mapping data for processing subforms
* @param array $subform A single subform entry
* @param string $table The table name used for linking values
*
* @return bool
* @since 3.2.2
*/
private function processSetSubform(array $setMap, array $subform, string $table): bool
{
$status = true;
foreach ($setMap as $key => $map)
{
if (!is_array($map) || !isset($subform[$key]))
{
continue;
}
if (!$this->processSetMap($subform, $map, $key, $table))
{
$status = false;
}
}
return $status;
}
/**
* Process a given map by either fetching nested subforms or handling them directly.
*
* @param array &$subform Reference to subform data
* @param array $map Map configuration for subform processing
* @param string $key Key associated with the map
* @param string $table Core table name for linking values
*
* @return void
* @since 3.2.2
*/
private function processGetMap(array &$subform, array $map, string $key, string $table): void
{
if (isset($map['_core']))
{
$this->handleCoreGetMap($subform, $map, $key, $table);
}
else
{
$this->handleRegularGetMap($subform, $map, $key, $table);
}
}
/**
* Process a given map by either setting nested subforms or handling them directly.
*
* @param array $subform Subform data
* @param array $map Map configuration for subform processing
* @param string $key Key associated with the map
* @param string $table Core table name for linking values
*
* @return bool
* @since 3.2.2
*/
private function processSetMap(array $subform, array $map, string $key, string $table): bool
{
if (isset($map['_core']))
{
return $this->handleCoreSetMap($subform, $map, $key, $table);
}
return $this->handleRegularSetMap($subform, $map, $key, $table);
}
/**
* Handle the processing of '_core' maps in a subform.
*
* @param array &$subform Reference to subform data
* @param array $map Map configuration for core subform processing
* @param string $key Key associated with the map
* @param string $table Core table name for linking values
*
* @return void
* @since 3.2.2
*/
private function handleCoreGetMap(array &$subform, array $map, string $key, string $table): void
{
if (is_array($map['_core']) && $this->validGetMap($map['_core']))
{
$map['_core']['linkValue'] = $this->setLinkValue($map['_core']['linkValue'], [$table => $subform]);
$subCoreData = $this->get($map);
if ($subCoreData !== null)
{
$subform[$key] = $subCoreData;
}
}
}
/**
* Handle the processing of '_core' maps in a subform.
*
* @param array $subform Subform data
* @param array $map Map configuration for core subform processing
* @param string $key Key associated with the map
* @param string $table Core table name for linking values
*
* @return bool
* @since 3.2.2
*/
private function handleCoreSetMap(array $subform, array $map, string $key, string $table): bool
{
if (is_array($map['_core']) && $this->validGetMap($map['_core']))
{
$map['_core']['linkValue'] = $this->setLinkValue($map['_core']['linkValue'], [$table => $subform]);
return $this->set($subform[$key], $map);
}
return false;
}
/**
* Handle the processing of regular maps in a subform.
*
* @param array &$subform Reference to subform data
* @param array $map Map configuration for regular subform processing
* @param string $key Key associated with the map
* @param string $table Core table name for linking values
*
* @return void
* @since 3.2.2
*/
private function handleRegularGetMap(array &$subform, array $map, string $key, string $table): void
{
$map['field'] = $key;
if ($this->validGetMap($map))
{
$subformData = $this->getSubformData($map, [$table => $subform]);
if ($subformData !== null)
{
$subform[$key] = $subformData;
}
}
}
/**
* Handle the processing of regular maps in a subform.
*
* @param array $subform Subform data
* @param array $map Map configuration for regular subform processing
* @param string $key Key associated with the map
* @param string $table Core table name for linking values
*
* @return bool
* @since 3.2.2
*/
private function handleRegularSetMap(array $subform, array $map, string $key, string $table): bool
{
if ($this->validSetMap($map))
{
return $this->setSubformData($subform[$key], $map, [$table => $subform]);
}
return false;
}
/**
* Validate the get map configuration for fetching subform data.
* Ensures all required keys are present and have valid values.
*
* @param array $map The map configuration to validate.
*
* @return bool Returns true if the map is valid, false otherwise.
* @since 3.2.2
*/
private function validGetMap(array $map): bool
{
// List of required keys with their expected types or validation functions
$requiredKeys = [
'table' => 'is_string',
'linkValue' => 'is_string',
'linkKey' => 'is_string',
'field' => 'is_string',
'get' => 'is_array'
];
// Iterate through each required key and validate
foreach ($requiredKeys as $key => $validator)
{
if (empty($map[$key]) || !$validator($map[$key]))
{
return false; // Key missing or validation failed
}
}
return true; // All checks passed
}
/**
* Validate the set map configuration for fetching subform data.
* Ensures all required keys are present and have valid values.
*
* @param array $map The map configuration to validate.
*
* @return bool Returns true if the map is valid, false otherwise.
* @since 3.2.2
*/
private function validSetMap(array $map): bool
{
// List of required keys with their expected types or validation functions
$requiredKeys = [
'table' => 'is_string',
'indexKey' => 'is_string',
'linkKey' => 'is_string',
'linkValue' => 'is_string'
];
// Iterate through each required key and validate
foreach ($requiredKeys as $key => $validator)
{
if (empty($map[$key]) || !$validator($map[$key]))
{
return false; // Key missing or validation failed
}
}
return true; // All checks passed
}
}

View File

@@ -0,0 +1,310 @@
<?php
/**
* @package Joomla.Component.Builder
*
* @created 3rd September, 2020
* @author Llewellyn van der Merwe <https://dev.vdm.io>
* @git Joomla Component Builder <https://git.vdm.dev/joomla/Component-Builder>
* @copyright Copyright (C) 2015 Vast Development Method. All rights reserved.
* @license GNU General Public License version 2 or later; see LICENSE.txt
*/
namespace VDM\Joomla\Data;
use VDM\Joomla\Interfaces\Data\ItemsInterface as Items;
use VDM\Joomla\Interfaces\Data\SubformInterface;
/**
* CRUD the data of any sub-form to another view (table)
*
* @since 3.2.2
*/
final class Subform implements SubformInterface
{
/**
* The ItemsInterface Class.
*
* @var Items
* @since 3.2.2
*/
protected Items $items;
/**
* Table Name
*
* @var string
* @since 3.2.1
*/
protected string $table;
/**
* Constructor.
*
* @param Items $items The ItemsInterface Class.
* @param string|null $table The table name.
*
* @since 3.2.2
*/
public function __construct(Items $items, ?string $table = null)
{
$this->items = $items;
if ($table !== null)
{
$this->table = $table;
}
}
/**
* Set the current active table
*
* @param string $table The table that should be active
*
* @return self
* @since 3.2.2
*/
public function table(string $table): self
{
$this->table = $table;
return $this;
}
/**
* Get a subform items
*
* @param string $linkValue The value of the link key in child table.
* @param string $linkKey The link key on which the items where linked in the child table.
* @param string $field The parent field name of the subform in the parent view.
* @param array $get The array get:set of the keys of each row in the subform.
*
* @return array|null The subform
* @since 3.2.2
*/
public function get(string $linkValue, string $linkKey, string $field, array $get): ?array
{
if (($items = $this->items->table($this->getTable())->get([$linkValue], $linkKey)) !== null)
{
return $this->converter($items, $get, $field);
}
return null;
}
/**
* Set a subform items
*
* @param array $items The list of items from the subform to set
* @param string $indexKey The index key on which the items should be observed as it relates to insert/update/delete.
* @param string $linkKey The link key on which the items where linked in the child table.
* @param string $linkValue The value of the link key in child table.
*
* @return bool
* @since 3.2.2
*/
public function set(array $items, string $indexKey, string $linkKey, string $linkValue): bool
{
$items = $this->process($items, $indexKey, $linkKey, $linkValue);
$this->purge($items, $indexKey, $linkKey, $linkValue);
return $this->items->table($this->getTable())->set(
$items, $indexKey
);
}
/**
* Get the current active table
*
* @return string
* @since 3.2.2
*/
public function getTable(): string
{
return $this->table;
}
/**
* Purge all items no longer in subform
*
* @param array $items The list of items to set.
* @param string $indexKey The index key on which the items should be observed as it relates to insert/update/delete
* @param string $linkKey The link key on which the items where linked in the child table.
* @param string $linkValue The value of the link key in child table.
*
* @return void
* @since 3.2.2
*/
private function purge(array $items, string $indexKey, string $linkKey, string $linkValue): void
{
// Get the current index values from the database
$currentIndexValues = $this->items->table($this->getTable())->values([$linkValue], $linkKey, $indexKey);
if ($currentIndexValues !== null)
{
// Extract the index values from the items array
$activeIndexValues = array_values(array_map(function($item) use ($indexKey) {
return $item[$indexKey] ?? null;
}, $items));
// Find the index values that are no longer in the items array
$inactiveIndexValues = array_diff($currentIndexValues, $activeIndexValues);
// Delete the inactive index values
if (!empty($inactiveIndexValues))
{
$this->items->table($this->getTable())->delete($inactiveIndexValues, $indexKey);
}
}
}
/**
* Filters the specified keys from an array of objects or arrays, converts them to arrays,
* and sets them by association with a specified key and an incrementing integer.
*
* @param array $items Array of objects or arrays to be filtered.
* @param array $keySet Array of keys to retain in each item.
* @param string $field The field prefix for the resulting associative array.
*
* @return array Array of filtered arrays set by association.
* @since 3.2.2
*/
private function converter(array $items, array $keySet, string $field): array
{
/**
* Filters keys for a single item and converts it to an array.
*
* @param object|array $item The item to filter.
* @param array $keySet The keys to retain.
*
* @return array The filtered array.
* @since 3.2.2
*/
$filterKeys = function ($item, array $keySet) {
$filteredArray = [];
foreach ($keySet as $key) {
if (is_object($item) && property_exists($item, $key)) {
$filteredArray[$key] = $item->{$key};
} elseif (is_array($item) && array_key_exists($key, $item)) {
$filteredArray[$key] = $item[$key];
}
}
return $filteredArray;
};
$result = [];
foreach ($items as $index => $item)
{
$filteredArray = $filterKeys($item, $keySet);
$result[$field . $index] = $filteredArray;
}
return $result;
}
/**
* Processes an array of arrays based on the specified key.
*
* @param array $items Array of arrays to be processed.
* @param string $indexKey The index key on which the items should be observed as it relates to insert/update/delete
* @param string $linkKey The link key on which the items where linked in the child table.
* @param string $linkValue The value of the link key in child table.
*
* @return array The processed array of arrays.
* @since 3.2.2
*/
private function process(array $items, string $indexKey, string $linkKey, string $linkValue): array
{
foreach ($items as &$item)
{
$value = $item[$indexKey] ?? '';
switch ($indexKey) {
case 'guid':
if (empty($value))
{
// set INDEX
$item[$indexKey] = $this->setGuid($indexKey);
}
break;
case 'id':
if (empty($value))
{
$item[$indexKey] = 0;
}
break;
default:
// No action for other keys if empty
break;
}
// set LINK
$item[$linkKey] = $linkValue;
}
return array_values($items);
}
/**
* Returns a GUIDv4 string
*
* Thanks to Dave Pearson (and other)
* https://www.php.net/manual/en/function.com-create-guid.php#119168
*
* Uses the best cryptographically secure method
* for all supported platforms with fallback to an older,
* less secure version.
*
* @param string $key The key to check and modify values.
* @param bool $trim
*
* @return string
*
* @since 3.0.9
*/
private function setGuid(string $key, bool $trim = true): string
{
// Windows
if (function_exists('com_create_guid'))
{
if ($trim)
{
return trim(\com_create_guid(), '{}');
}
return \com_create_guid();
}
// set the braces if needed
$lbrace = $trim ? "" : chr(123); // "{"
$rbrace = $trim ? "" : chr(125); // "}"
// OSX/Linux
if (function_exists('openssl_random_pseudo_bytes'))
{
$data = \openssl_random_pseudo_bytes(16);
$data[6] = chr( ord($data[6]) & 0x0f | 0x40); // set version to 0100
$data[8] = chr( ord($data[8]) & 0x3f | 0x80); // set bits 6-7 to 10
return $lbrace . vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)) . $lbrace;
}
// Fallback (PHP 4.2+)
mt_srand((double) microtime() * 10000);
$charid = strtolower( md5( uniqid( rand(), true)));
$hyphen = chr(45); // "-"
$guidv4 = $lbrace.
substr($charid, 0, 8). $hyphen.
substr($charid, 8, 4). $hyphen.
substr($charid, 12, 4). $hyphen.
substr($charid, 16, 4). $hyphen.
substr($charid, 20, 12).
$rbrace;
// check that it does not already exist (one in a billion chance ;)
// but we do it any way...
if ($this->items->table($this->getTable())->values([$guidv4], $key))
{
return $this->setGuid($key);
}
return $guidv4;
}
}

View File

@@ -0,0 +1 @@
<html><body bgcolor="#FFFFFF"></body></html>