summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--config.example.toml1
-rw-r--r--public/index.php21
-rw-r--r--src/App.php350
-rw-r--r--src/Builder.php80
-rw-r--r--src/Controllers/EntriesController.php84
-rw-r--r--src/Controllers/FieldsController.php25
-rw-r--r--src/Controllers/SubmissionController.php49
-rw-r--r--src/Controllers/ValidationController.php26
-rw-r--r--src/HookManager.php42
-rw-r--r--src/HttpException.php6
-rw-r--r--src/PluginLoader.php29
-rw-r--r--src/PreLoader.php15
-rw-r--r--src/Utilities.php40
-rw-r--r--src/Validator.php94
15 files changed, 550 insertions, 313 deletions
diff --git a/.gitignore b/.gitignore
index a23bab3..da8b7b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
/vendor/
/content/
+/plugins/
/config.toml
diff --git a/config.example.toml b/config.example.toml
index 64ecc4a..7d589d1 100644
--- a/config.example.toml
+++ b/config.example.toml
@@ -1,2 +1,3 @@
[app]
contentFolderPath = './content'
+pluginsFolderPath = './plugins'
diff --git a/public/index.php b/public/index.php
index b943298..8a7ec7d 100644
--- a/public/index.php
+++ b/public/index.php
@@ -1,11 +1,16 @@
<?php
use FlatFileForms\App;
+use FlatFileForms\PluginLoader;
+use FlatFileForms\PreLoader;
use Yosymfony\Toml\Toml;
require_once dirname(__DIR__) . '/vendor/autoload.php';
-function findAppConfigFile($path)
+/**
+ * Find the config file
+ */
+function findAppConfigFile(string $path): string
{
$currentDirectory = $path;
while ($currentDirectory !== '/') {
@@ -20,17 +25,31 @@ function findAppConfigFile($path)
die('config.toml missing');
}
+// find and parse config
$configFile = findAppConfigFile(dirname(__DIR__));
$config = Toml::parseFile($configFile);
+// prepare possibly relative folders path
chdir(dirname($configFile));
+
$contentDirectoryPath = realpath($config['app']['contentFolderPath']);
$contentDirectoryPath === false && die('Content folder "' . $config['app']['contentFolderPath'] . '" missing');
$config['app']['contentFolderPath'] = $contentDirectoryPath;
+
+$pluginsDirectoryPath = realpath($config['app']['pluginsFolderPath']);
+$pluginsDirectoryPath === false && die('Plugins folder "' . $config['app']['pluginsFolderPath'] . '" missing');
+$config['app']['pluginsFolderPath'] = $pluginsDirectoryPath;
+
chdir($_SERVER['DOCUMENT_ROOT']);
+// set config values to global $_ENV
foreach ($config as $key => $value) {
$_ENV[$key] = $value;
}
+// load
+new PreLoader();
+new PluginLoader();
+
+// run
new App();
diff --git a/src/App.php b/src/App.php
index 92ceb4a..15be58b 100644
--- a/src/App.php
+++ b/src/App.php
@@ -2,6 +2,10 @@
namespace FlatFileForms;
+use FlatFileForms\Controllers\EntriesController;
+use FlatFileForms\Controllers\FieldsController;
+use FlatFileForms\Controllers\SubmissionController;
+use FlatFileForms\Controllers\ValidationController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Yosymfony\Toml\Toml;
@@ -33,104 +37,34 @@ class App
// check api key
$apiKey = $_GET['key'] ?? $_POST['key'] ?? null;
if (empty($apiKey)) {
- $response->setStatusCode(Response::HTTP_BAD_REQUEST);
- throw new \Exception('API key missing');
+ throw new HttpException('API key missing', Response::HTTP_BAD_REQUEST);
}
if (! in_array($apiKey, $config['api']['keys'])) {
- $response->setStatusCode(Response::HTTP_UNAUTHORIZED);
- throw new \Exception('API key does not match');
+ throw new HttpException('API key does not match', Response::HTTP_UNAUTHORIZED);
}
// GET
if ($method == 'GET') {
if (str_ends_with($path, '/fields')) {
- $this->formPath = $formPath = $contentRoot . str_replace('/fields', '', $path);
+ $formPath = $contentRoot . str_replace('/fields', '', $path);
- $fields = $this->buildFields($formPath, $_GET['page'] ?? null);
+ $builder = new Builder($formPath);
- // flatten paged form
- if ($this->isPagedFieldSet($fields) && isset($_GET['flat'])) {
- $fields = array_merge(...array_values($fields));
- }
+ $fieldsController = new FieldsController();
- $content['data'] = $fields;
+ $content = $fieldsController->getFields($builder);
}
else if (str_ends_with($path, '/entries')) {
if (! isset($_GET['dateFrom'])) {
- $response->setStatusCode(Response::HTTP_BAD_REQUEST);
- throw new \Exception('dateFrom parameter missing');
+ throw new HttpException('dateFrom parameter missing', Response::HTTP_BAD_REQUEST);
}
- $this->formPath = $formPath = $contentRoot . str_replace('/entries', '', $path);
-
- $entries = [];
-
- $dateFrom = new \DateTime($_GET['dateFrom']);
- $dateTo = new \DateTime($_GET['dateTo'] ?? 'now');
-
- $dateRangeYears = range($dateFrom->format('Y'), $dateTo->format('Y'));
- $dateRangeYearsCount = count($dateRangeYears);
- foreach ($dateRangeYears as $dateRangeYearIdx => $dateRangeYear) {
- $yearPath = "$formPath/entries/$dateRangeYear";
- if (! is_dir($yearPath)) {
- continue;
- }
-
- if ($dateRangeYearsCount === 1) {
- $dateRangeMonths = range($dateFrom->format('m'), $dateTo->format('m'));
- }
- else if ($dateRangeYearIdx === 0) {
- $dateRangeMonths = range($dateFrom->format('m'), 12);
- }
- else if ($dateRangeYearIdx === $dateRangeYearsCount - 1) {
- $dateRangeMonths = range(1, $dateTo->format('m'));
- }
- else {
- $dateRangeMonths = range(1, 12);
- }
-
- $dateRangeMonthsCount = count($dateRangeMonths);
- foreach ($dateRangeMonths as $dateRangeMonthIdx => $dateRangeMonth) {
- $monthPath = "$yearPath/" . sprintf('%02d', $dateRangeMonth);
- if (! is_dir($monthPath)) {
- continue;
- }
-
- if ($dateRangeMonthsCount === 1) {
- $dateRangeDays = range($dateFrom->format('d'), $dateTo->format('d'));
- }
- else if ($dateRangeYearIdx === 0 && $dateRangeMonthIdx === 0) {
- $dateRangeDays = range($dateFrom->format('d'), 31);
- }
- else if ($dateRangeYearIdx === $dateRangeYearsCount - 1 && $dateRangeMonthIdx === $dateRangeMonthsCount - 1) {
- $dateRangeDays = range(1, $dateTo->format('d'));
- }
- else {
- $dateRangeDays = range(1, 31);
- }
-
- foreach ($dateRangeDays as $dateRangeDay) {
- $dayPath = "$monthPath/" . sprintf('%02d', $dateRangeDay);
- if (! is_dir($dayPath)) {
- continue;
- }
-
- $entriesForDay = $this->scandir($dayPath);
- foreach ($entriesForDay as $entryForDay) {
- $entry = Toml::parseFile("$dayPath/$entryForDay");
- if (isset($_GET['flat'])) {
- $entries[] = $entry;
- } else {
- $entries[$dateRangeYear][$dateRangeMonth][$dateRangeDay][] = $entry;
- }
- }
- }
-
- }
- }
+ $formPath = $contentRoot . str_replace('/entries', '', $path);
+
+ $entriesController = new EntriesController();
- $content['data'] = $entries;
+ $content = $entriesController->getEntries();
}
else {
@@ -141,46 +75,36 @@ class App
// POST
else if ($method == 'POST') {
if (str_ends_with($path, '/validate')) {
- $this->formPath = $formPath = $contentRoot . str_replace('/validate', '', $path);
+ $formPath = $contentRoot . str_replace('/validate', '', $path);
- $fields = $this->buildFields($formPath, $_GET['page'] ?? null);
+ $builder = new Builder($formPath);
+ $validator = new Validator($formPath);
- $content = $this->validateRequest($fields);
+ $validationController = new ValidationController();
+
+ $content = $validationController->validateRequest($builder, $validator);
}
else if (str_ends_with($path, '/submit')) {
- $this->formPath = $formPath = $contentRoot . str_replace('/submit', '', $path);
-
- $fields = $this->buildFields($formPath);
-
- $content = $this->validateRequest($fields);
-
- // if there were no validation errors then add entry
- if (empty($content['error'])) {
- $date = new \Datetime();
- $entry = [
- 'fields' => $_POST,
- 'date' => $date->format('c'),
- ];
-
- $builder = new TomlBuilder();
- $builder->addValue('date', $entry['date']);
- $builder->addTable('fields');
- foreach ($entry['fields'] as $entryKey => $entryValue) {
- $builder->addValue($entryKey, $entryValue);
- }
-
- $entryDirectory = $formPath . '/entries/' . $date->format('Y/m/d');
- @mkdir($entryDirectory, 0774, true);
- $entryFilename = $date->format('Ymd_Hi_') . hash('adler32', serialize($entry)) . '.toml';
- file_put_contents(
- $entryDirectory . '/' . $entryFilename,
- $builder->getTomlString()
- );
+ $formPath = $contentRoot . str_replace('/submit', '', $path);
+
+ $builder = new Builder($formPath);
+ $validator = new Validator($formPath);
+
+ $submissionController = new SubmissionController();
+
+ $content = $submissionController->submit($builder, $validator, $formPath);
+
+ if (! empty($content['error'])) {
+ throw new HttpException($content['error'], Response::HTTP_UNPROCESSABLE_ENTITY);
}
}
}
} catch (\Exception $exception) {
+ if ($exception instanceof HttpException) {
+ $response->setStatusCode($exception->getCode());
+ }
+
$content['error'] = basename(get_class($exception)) . ': ' . $exception->getMessage();
}
@@ -190,178 +114,10 @@ class App
$response->send();
}
- /**
- * @param array $fields
- */
- public function validateRequest($fields) {
- $content = [];
- $hasInvalidFields = false;
-
- $fields = $this->validateFields($fields);
-
- if ($this->isPagedFieldSet($fields)) {
- // remove surplus field values from response
- $fields = array_map(function ($page) {
- return array_map(function ($field) {
- return array_intersect_key($field, array_flip([
- 'is_valid',
- ]));
- }, $page);
- }, $fields);
-
- $flattened = array_merge(...array_values($fields));
- $hasInvalidFields = in_array(false, array_column($flattened, 'is_valid'));
- } else {
- // remove surplus field values from response
- $fields = array_map(function ($field) {
- return array_intersect_key($field, array_flip([
- 'is_valid',
- ]));
- }, $fields);
-
- $hasInvalidFields = in_array(false, array_column($fields, 'is_valid'));
- }
-
- $content['data'] = $fields;
-
- if ($hasInvalidFields) {
- $content['error'] = 'Invalid fields';
- }
-
- return $content;
- }
-
- /**
- * @param string $formPath
- * @param mixed $page
- */
- public function buildFields($formPath, $page = null)
- {
- $parsed = Toml::parseFile($formPath . '/fields/_fields.toml');
- $fields = [];
-
- // if a page is requested
- if ($page) {
- if (! isset($parsed['page'])) {
- throw new \Exception('Form has no pages');
- }
-
- if (! isset($parsed['page'][$page])) {
- throw new \Exception('Form has no page ' . $page);
- }
-
- $fields = $this->buildSinglePageFields($parsed['page'][$page], $formPath);
- }
-
- // else get all fields
- else {
- // if form is paged
- if (isset($parsed['page'])) {
- $pages = $parsed['page'];
- foreach ($pages as $pageKey => $pageFields) {
- $fields[$pageKey] = $this->buildSinglePageFields($pageFields, $formPath);
- }
- }
-
- // if form is not paged
- else {
- foreach ($parsed['field'] as $key => $field) {
- $fields[$key] = $this->buildSingleField($formPath, $key, $field);
- }
- }
- }
-
- return $fields;
- }
-
- /**
- * @param array $pageFields
- * @param string $formPath
- */
- public function buildSinglePageFields($pageFields, $formPath)
- {
- $fields = [];
-
- if (! empty($pageFields['file'])) {
- $pageFields = array_replace_recursive($pageFields, Toml::parseFile($formPath . '/fields/' . $pageFields['file']));
- }
-
- foreach ($pageFields['field'] as $key => $field) {
- $fields[$key] = $this->buildSingleField($formPath, $key, $field);
- }
-
- return $fields;
- }
-
- /**
- * @param string $formPath
- * @param string $key
- * @param string $field
- */
- public function buildSingleField($formPath, $key, $field)
- {
- if (! empty($field['file'])) {
- $field = array_replace_recursive($field, Toml::parseFile($formPath . '/fields/' . $field['file']));
- }
-
- if (empty($field['name'])) {
- $field['name'] = $key;
- }
-
- return $field;
- }
-
- /**
- * @param array $fields
- */
- public function validateFields($fields)
- {
- if ($this->isPagedFieldSet($fields)) {
- foreach ($fields as $pageKey => &$pageFields) {
- foreach ($pageFields as $key => &$field) {
- $field = $this->validateSingleField($field);
- }
- }
- } else {
- foreach ($fields as $key => &$field) {
- $field = $this->validateSingleField($field);
- }
- }
-
- return $fields;
- }
-
- /**
- * @param array $field
- */
- public function validateSingleField($field)
- {
- $value = $_POST[$field['name']] ?? '';
- $field['is_valid'] = true;
-
- if (isset($field['required']) && empty($value)) {
- $field['is_valid'] = false;
- }
-
- if (isset($field['validation']['pattern']) && preg_match_all('/' . $field['validation']['pattern'] . '/', $value) === 0) {
- $field['is_valid'] = false;
- }
-
- $validationFunctionName = 'validate_' . basename($this->formPath) . '_' . $field['name'];
- if (function_exists($validationFunctionName)) {
- $field = call_user_func($validationFunctionName, $field, $value);
- }
-
- return $field;
- }
-
- /**
- * @param string $formPath
- */
- public function buildConfig($formPath)
+ public function buildConfig(string $requestPath): array
{
$config = [];
- $currentDirectory = $formPath;
+ $currentDirectory = $requestPath;
while (true) {
$configFile = $currentDirectory . '/config/config.toml';
if (file_exists($configFile)) {
@@ -387,34 +143,4 @@ class App
return $config;
}
-
- /**
- * @param array $fields
- */
- public function isPagedFieldSet($fields)
- {
- $firstItem = reset($fields);
-
- return ! (isset($firstItem['name']) && ! is_array($firstItem['name']));
- }
-
- /**
- * @param string|array $paths
- */
- public function scandir($paths)
- {
- $paths = (array)$paths;
-
- $scanned = [];
- foreach ($paths as $path) {
- $filtered = array_values(
- array_filter(
- scandir($path), fn ($item) => ! in_array($item, ['.', '..'])
- )
- );
- array_push($scanned, ...$filtered);
- }
-
- return $scanned;
- }
}
diff --git a/src/Builder.php b/src/Builder.php
new file mode 100644
index 0000000..308fa82
--- /dev/null
+++ b/src/Builder.php
@@ -0,0 +1,80 @@
+<?php
+
+namespace FlatFileForms;
+
+use Yosymfony\Toml\Toml;
+
+class Builder
+{
+ public function __construct(
+ private string $formPath
+ )
+ {}
+
+ public function buildFields(mixed $page = null)
+ {
+ $parsed = Toml::parseFile($this->formPath . '/fields/_fields.toml');
+ $fields = [];
+
+ // if a page is requested
+ if ($page) {
+ if (! isset($parsed['page'])) {
+ throw new \Exception('Form has no pages');
+ }
+
+ if (! isset($parsed['page'][$page])) {
+ throw new \Exception('Form has no page ' . $page);
+ }
+
+ $fields = $this->buildSinglePageFields($parsed['page'][$page]);
+ }
+
+ // else get all fields
+ else {
+ // if form is paged
+ if (isset($parsed['page'])) {
+ $pages = $parsed['page'];
+ foreach ($pages as $pageKey => $pageFields) {
+ $fields[$pageKey] = $this->buildSinglePageFields($pageFields);
+ }
+ }
+
+ // if form is not paged
+ else {
+ foreach ($parsed['field'] as $key => $field) {
+ $fields[$key] = $this->buildSingleField($key, $field);
+ }
+ }
+ }
+
+ return $fields;
+ }
+
+ public function buildSinglePageFields(array $pageFields): array
+ {
+ $fields = [];
+
+ if (! empty($pageFields['file'])) {
+ $pageFields = array_replace_recursive($pageFields, Toml::parseFile($this->formPath . '/fields/' . $pageFields['file']));
+ }
+
+ foreach ($pageFields['field'] as $key => $field) {
+ $fields[$key] = $this->buildSingleField($key, $field);
+ }
+
+ return $fields;
+ }
+
+ public function buildSingleField(string $key, array $field): array
+ {
+ if (! empty($field['file'])) {
+ $field = array_replace_recursive($field, Toml::parseFile($this->formPath . '/fields/' . $field['file']));
+ }
+
+ if (empty($field['name'])) {
+ $field['name'] = $key;
+ }
+
+ return $field;
+ }
+}
diff --git a/src/Controllers/EntriesController.php b/src/Controllers/EntriesController.php
new file mode 100644
index 0000000..13aa8e0
--- /dev/null
+++ b/src/Controllers/EntriesController.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace FlatFileForms\Controllers;
+
+use Yosymfony\Toml\Toml;
+
+class EntriesController
+{
+ public function getEntries()
+ {
+ /**@var Utilities $utilities*/
+ global $utilities;
+
+ $entries = [];
+
+ $dateFrom = new \DateTime($_GET['dateFrom']);
+ $dateTo = new \DateTime($_GET['dateTo'] ?? 'now');
+
+ $dateRangeYears = range($dateFrom->format('Y'), $dateTo->format('Y'));
+ $dateRangeYearsCount = count($dateRangeYears);
+ foreach ($dateRangeYears as $dateRangeYearIdx => $dateRangeYear) {
+ $yearPath = "$formPath/entries/$dateRangeYear";
+ if (! is_dir($yearPath)) {
+ continue;
+ }
+
+ if ($dateRangeYearsCount === 1) {
+ $dateRangeMonths = range($dateFrom->format('m'), $dateTo->format('m'));
+ }
+ else if ($dateRangeYearIdx === 0) {
+ $dateRangeMonths = range($dateFrom->format('m'), 12);
+ }
+ else if ($dateRangeYearIdx === $dateRangeYearsCount - 1) {
+ $dateRangeMonths = range(1, $dateTo->format('m'));
+ }
+ else {
+ $dateRangeMonths = range(1, 12);
+ }
+
+ $dateRangeMonthsCount = count($dateRangeMonths);
+ foreach ($dateRangeMonths as $dateRangeMonthIdx => $dateRangeMonth) {
+ $monthPath = "$yearPath/" . sprintf('%02d', $dateRangeMonth);
+ if (! is_dir($monthPath)) {
+ continue;
+ }
+
+ if ($dateRangeMonthsCount === 1) {
+ $dateRangeDays = range($dateFrom->format('d'), $dateTo->format('d'));
+ }
+ else if ($dateRangeYearIdx === 0 && $dateRangeMonthIdx === 0) {
+ $dateRangeDays = range($dateFrom->format('d'), 31);
+ }
+ else if ($dateRangeYearIdx === $dateRangeYearsCount - 1 && $dateRangeMonthIdx === $dateRangeMonthsCount - 1) {
+ $dateRangeDays = range(1, $dateTo->format('d'));
+ }
+ else {
+ $dateRangeDays = range(1, 31);
+ }
+
+ foreach ($dateRangeDays as $dateRangeDay) {
+ $dayPath = "$monthPath/" . sprintf('%02d', $dateRangeDay);
+ if (! is_dir($dayPath)) {
+ continue;
+ }
+
+ $entriesForDay = $utilities->scandir($dayPath);
+ foreach ($entriesForDay as $entryForDay) {
+ $entry = Toml::parseFile("$dayPath/$entryForDay");
+ if (isset($_GET['flat'])) {
+ $entries[] = $entry;
+ } else {
+ $entries[$dateRangeYear][$dateRangeMonth][$dateRangeDay][] = $entry;
+ }
+ }
+ }
+
+ }
+ }
+
+ $content['data'] = $entries;
+
+ return $content;
+ }
+}
diff --git a/src/Controllers/FieldsController.php b/src/Controllers/FieldsController.php
new file mode 100644
index 0000000..c407604
--- /dev/null
+++ b/src/Controllers/FieldsController.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace FlatFileForms\Controllers;
+
+use FlatFileForms\Builder;
+
+class FieldsController
+{
+ public function getFields(Builder $builder): array
+ {
+ /**@var Utilities $utilities*/
+ global $utilities;
+
+ $fields = $builder->buildFields($_GET['page'] ?? null);
+
+ // flatten paged form
+ if ($utilities->isPagedFieldSet($fields) && isset($_GET['flat'])) {
+ $fields = array_merge(...array_values($fields));
+ }
+
+ $content['data'] = $fields;
+
+ return $content;
+ }
+}
diff --git a/src/Controllers/SubmissionController.php b/src/Controllers/SubmissionController.php
new file mode 100644
index 0000000..f2c4c80
--- /dev/null
+++ b/src/Controllers/SubmissionController.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace FlatFileForms\Controllers;
+
+use FlatFileForms\Builder;
+use FlatFileForms\Validator;
+use Yosymfony\Toml\TomlBuilder;
+
+class SubmissionController
+{
+ public function submit(Builder $builder, Validator $validator, $formPath)
+ {
+ $fields = $builder->buildFields();
+
+ // run through validation
+ $result = $validator->validateRequest($fields);
+
+ $content['data'] = $result['fields'];
+
+ // if there were no validation errors then add entry
+ if (empty($result['error'])) {
+ $date = new \Datetime();
+ $entry = [
+ 'fields' => $_POST,
+ 'date' => $date->format('c'),
+ ];
+
+ $entryBuilder = new TomlBuilder();
+ $entryBuilder->addValue('date', $entry['date']);
+ $entryBuilder->addTable('fields');
+ foreach ($entry['fields'] as $entryKey => $entryValue) {
+ $entryBuilder->addValue($entryKey, $entryValue);
+ }
+
+ $entryDirectory = $formPath . '/entries/' . $date->format('Y/m/d');
+ @mkdir($entryDirectory, 0774, true);
+ $entryFilename = $date->format('Ymd_Hi_') . hash('adler32', serialize($entry)) . '.toml';
+ file_put_contents(
+ $entryDirectory . '/' . $entryFilename,
+ $entryBuilder->getTomlString()
+ );
+ }
+ else {
+ $content['error'] = $result['error'];
+ }
+
+ return $content;
+ }
+}
diff --git a/src/Controllers/ValidationController.php b/src/Controllers/ValidationController.php
new file mode 100644
index 0000000..f90359a
--- /dev/null
+++ b/src/Controllers/ValidationController.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace FlatFileForms\Controllers;
+
+use FlatFileForms\Builder;
+use FlatFileForms\Validator;
+
+class ValidationController
+{
+ public function validateRequest(Builder $builder, Validator $validator): array
+ {
+ /**@var Utilities $utilities*/
+ global $utilities;
+
+ $fields = $builder->buildFields($_GET['page'] ?? null);
+
+ $result = $validator->validateRequest($fields);
+
+ $content['data'] = $result['fields'];
+ if (! empty($result['error'])) {
+ $content['error'] = $result['error'];
+ }
+
+ return $content;
+ }
+}
diff --git a/src/HookManager.php b/src/HookManager.php
new file mode 100644
index 0000000..96ca8d3
--- /dev/null
+++ b/src/HookManager.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace FlatFileForms;
+
+class HookManager
+{
+ private array $actions = [];
+ private array $filters = [];
+
+ public function addAction(string $name, callable $function, int $priority = 10): void
+ {
+ $this->action[$name][$priority][] = compact('name', 'function', 'priority');
+ }
+
+ public function addFilter(string $name, callable $function, int $priority = 10): void
+ {
+ $this->filters[$name][$priority][] = compact('name', 'callable', 'priority');
+ }
+
+ public function doAction(string $name, mixed ...$arguments): void
+ {
+ foreach ($this->actions[$name] as $actions) {
+ foreach ($actions as $action) {
+ call_user_func_array($action['function'], $arguments);
+ }
+ }
+ }
+
+ public function applyFilter(string $name, mixed $value, mixed ...$arguments): mixed
+ {
+ // set $value as first argument
+ array_unshift($arguments, $value);
+
+ foreach ($this->filters[$name] as $filters) {
+ foreach ($filters as $filter) {
+ $value = call_user_func_array($filter['function'], $arguments);
+ }
+ }
+
+ return $value;
+ }
+}
diff --git a/src/HttpException.php b/src/HttpException.php
new file mode 100644
index 0000000..85d2854
--- /dev/null
+++ b/src/HttpException.php
@@ -0,0 +1,6 @@
+<?php
+
+namespace FlatFileForms;
+
+class HttpException extends \Exception
+{}
diff --git a/src/PluginLoader.php b/src/PluginLoader.php
new file mode 100644
index 0000000..c8da5fd
--- /dev/null
+++ b/src/PluginLoader.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace FlatFileForms;
+
+class PluginLoader
+{
+ public function __construct()
+ {
+ /**@var Utilities $utilities*/
+ global $utilities;
+
+ $pluginsDirectoryPath = $_ENV['app']['pluginsFolderPath'];
+
+ spl_autoload_register(function ($classname) use ($pluginsDirectoryPath) {
+ $classname = str_replace('FlatFileForms\\Plugins\\', '', $classname);
+
+ require_once
+ $pluginsDirectoryPath . '/' .
+ str_replace('\\', '/', $classname) .
+ '.php';
+ });
+
+ $pluginDirectories = $utilities->scandir($pluginsDirectoryPath);
+ foreach ($pluginDirectories as $directory) {
+ $pluginClass = 'FlatFileForms\\Plugins\\' . $directory . '\\Plugin';
+ $plugin = new $pluginClass();
+ }
+ }
+}
diff --git a/src/PreLoader.php b/src/PreLoader.php
new file mode 100644
index 0000000..57a0f52
--- /dev/null
+++ b/src/PreLoader.php
@@ -0,0 +1,15 @@
+<?php
+
+namespace FlatFileForms;
+
+class PreLoader
+{
+ public function __construct()
+ {
+ global $utilities;
+ $utilities = new Utilities();
+
+ global $hooks;
+ $hooks = new HookManager();
+ }
+}
diff --git a/src/Utilities.php b/src/Utilities.php
new file mode 100644
index 0000000..0462ad6
--- /dev/null
+++ b/src/Utilities.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace FlatFileForms;
+
+class Utilities
+{
+ public function isPagedFieldSet(array $fields): bool
+ {
+ $firstItem = reset($fields);
+
+ return ! (isset($firstItem['name']) && ! is_array($firstItem['name']));
+ }
+
+ public function scandir(string $path): array
+ {
+ $path = rtrim($path, '/');
+
+ return array_values(
+ array_map(
+ fn ($item) => $path . '/' . $item,
+ array_filter(
+ scandir($path), fn ($item) => ! in_array($item, ['.', '..'])
+ )
+ )
+ );
+ }
+
+ public function scandirMultiple(string|array $paths): array
+ {
+ $paths = (array)$paths;
+
+ $merged = [];
+ foreach ($paths as $path) {
+ $scanned = $this->scandir($path);
+ array_push($merged, ...$scanned);
+ }
+
+ return $merged;
+ }
+}
diff --git a/src/Validator.php b/src/Validator.php
new file mode 100644
index 0000000..33cdf34
--- /dev/null
+++ b/src/Validator.php
@@ -0,0 +1,94 @@
+<?php
+
+namespace FlatFileForms;
+
+class Validator
+{
+ public function __construct(
+ private string $formPath
+ )
+ {}
+
+ public function validateRequest(array $fields): array
+ {
+ /**@var Utilities $utilities*/
+ global $utilities;
+
+ $result = [];
+ $hasInvalidFields = false;
+
+ $fields = $this->validateFields($fields);
+
+ if ($utilities->isPagedFieldSet($fields)) {
+ // remove surplus field values from response
+ $fields = array_map(function ($page) {
+ return array_map(function ($field) {
+ return array_intersect_key($field, array_flip([
+ 'is_valid',
+ ]));
+ }, $page);
+ }, $fields);
+
+ $flattened = array_merge(...array_values($fields));
+ $hasInvalidFields = in_array(false, array_column($flattened, 'is_valid'));
+ } else {
+ // remove surplus field values from response
+ $fields = array_map(function ($field) {
+ return array_intersect_key($field, array_flip([
+ 'is_valid',
+ ]));
+ }, $fields);
+
+ $hasInvalidFields = in_array(false, array_column($fields, 'is_valid'));
+ }
+
+ $result['fields'] = $fields;
+
+ if ($hasInvalidFields) {
+ $result['error'] = 'invalid fields';
+ }
+
+ return $result;
+ }
+
+ public function validateFields(array $fields): array
+ {
+ /**@var Utilities $utilities*/
+ global $utilities;
+
+ if ($utilities->isPagedFieldSet($fields)) {
+ foreach ($fields as $pageKey => &$pageFields) {
+ foreach ($pageFields as $key => &$field) {
+ $field = $this->validateSingleField($field);
+ }
+ }
+ } else {
+ foreach ($fields as $key => &$field) {
+ $field = $this->validateSingleField($field);
+ }
+ }
+
+ return $fields;
+ }
+
+ public function validateSingleField(array $field): array
+ {
+ $value = $_POST[$field['name']] ?? '';
+ $field['is_valid'] = true;
+
+ if (isset($field['required']) && empty($value)) {
+ $field['is_valid'] = false;
+ }
+
+ if (isset($field['validation']['pattern']) && preg_match_all('/' . $field['validation']['pattern'] . '/', $value) === 0) {
+ $field['is_valid'] = false;
+ }
+
+ $validationFunctionName = 'validate_' . basename($this->formPath) . '_' . $field['name'];
+ if (function_exists($validationFunctionName)) {
+ $field = call_user_func($validationFunctionName, $field, $value);
+ }
+
+ return $field;
+ }
+}