Implement attachments module with CRUD operations and integration points

- Added `attachments` module for creating, reading, updating, and deleting attachments.
- Introduced new classes such as `attachments`, `attachment_store`, `attachment_content`, and `attachment_relation` to handle attachment operations and their relationships.
- Integrated `attachments` features into `db_object_t` for seamless object-attachment interactions.
- Added `attachments_i` interface for standardized attachments module operations.
- Created `attachmentsRoute` for defining endpoints related to attachments.
- Enabled module configuration through `attachments_enabled_c` for attachment management control.
This commit is contained in:
Jeppe Bundgaard
2025-09-22 10:28:22 +02:00
parent 94bbf0a106
commit 33a26cffc5
13 changed files with 567 additions and 0 deletions
@@ -0,0 +1,86 @@
<?php
namespace classes;
use interfaces\minio_uploads_i;
use traits\minio_t;
class attachment_store implements minio_uploads_i
{
use minio_t;
public function __construct()
{
self::setBucket('attachments'); // Change the bucket to 'pdfs'
}
/**
* @inheritDoc
*/
public function generatePresignedUrl(string $objectName, int $expiry = 3600): string
{
// Generate a presigned URL for the given object name with the specified expiry time
return self::getPresignedUrl($objectName, $expiry, true);
}
/**
* @inheritDoc
*/
public function isValidFileName(string $fileName): bool
{
// Check if the file name is valid
return preg_match('/^[a-zA-Z0-9_\-.]+$/', $fileName) === 1;
}
/**
* @inheritDoc
*/
public function isValidFilePath(string $filePath): bool
{
// Check if the file path is valid
return preg_match('/^[a-zA-Z0-9_\-\/.]+$/', $filePath) === 1;
}
/**
* @inheritDoc
*/
public function requireValidFileName(string $fileName): void
{
if (!$this->isValidFileName($fileName)) {
throw new \InvalidArgumentException("Invalid file name: $fileName");
}
}
/**
* @inheritDoc
*/
public function requireValidFilePath(string $filePath): void
{
if (!$this->isValidFilePath($filePath)) {
throw new \InvalidArgumentException("Invalid file path: $filePath");
}
}
public function isFileInStore(string $fileName): bool
{
// Check if the file exists in the store
return self::doesObjectExist($fileName);
}
public function download(string $file): string
{
$path = '/tmp/' . $file;
$result = self::getS3Client()->getObject([
'Bucket' => self::getBucket(),
'Key' => $file,
'SaveAs' => $path
]);
return $path;
}
public function generateDirectDownloadUrl(string $fileName): string
{
// Generate a direct download URL for the given file name
return 'https://api.truckwash.dk:4433/files/' . $fileName;
}
}
@@ -0,0 +1,95 @@
<?php
namespace classes;
require_once WD . '/modules/attachments/attachments_c.php';
use attachments\helpers\attachment_content;
use Exception;
use interfaces\attachments_i;
use attachments\helpers\attachment;
use attachments\attachments_c;
use objects\object_attachments_o;
class attachments implements attachments_i
{
/**
* The configuration of the module
* @var attachments_c $config
*/
public attachments_c $config;
public function __construct()
{
$this->config = new attachments_c();
}
/**
* @inheritDoc
*/
public function list(string $type, int $object_id, array $options = []): array
{
if (count($options) === 0) {
$options = ['id', 'content', 'created_at', 'object_id', 'object_type', 'created_at', 'updated_at']; // Default fields to return
}
return array_map(function ($item) {
$item = (object)$item;
$item->content = json_decode($item->content, true); // Decode JSON content
return (new attachment())->populate((object)$item);
}, (new object_attachments_o())->getFieldsWhere([
'object_type' => $type,
'object_id' => $object_id,
'deleted_at' => null
], $options));
}
/**
* @inheritDoc
* @throws Exception If the attachment is not found
*/
public function get(int $attachment_id): ?object_attachments_o
{
return (new object_attachments_o())->select($attachment_id) ?: null;
}
/**
* @inheritDoc
* @throws Exception If the attachment cannot be created
*/
public function create(string $type, int $object_id, attachment_content $attachment_content): bool
{
$object_attachments_o = new object_attachments_o();
$object_attachments_o->add([
'object_type' => $type,
'object_id' => $object_id,
'content' => json_encode($attachment_content->toArray()), // Store as JSON
]);
return (bool)$object_attachments_o->id;
}
/**
* @inheritDoc
* @throws Exception If the attachment cannot be deleted
*/
public function delete(int $attachment_id): bool
{
$object_attachments_o = new object_attachments_o();
if ($object_attachments_o->select($attachment_id)) {
$object_attachments_o->delete();
return true;
}
return false;
}
/**
* @inheritDoc
* @throws Exception If the attachment cannot be updated
*/
public function update(int $attachment_id, attachment_content $attachment_content): bool
{
$object_attachments_o = new object_attachments_o();
if ($object_attachments_o->select($attachment_id)) {
$object_attachments_o->content->set(json_encode($attachment_content->toArray()));
return true;
}
return false;
}
}
+2
View File
@@ -73,6 +73,8 @@ require_once 'classes/openai.php';
require_once 'classes/licenseplaterecognizer.php';
require_once 'classes/image_processor.php';
require_once 'classes/upload_store.php';
require_once 'classes/attachments.php';
require_once 'classes/attachment_store.php';
/**
* Modules
@@ -0,0 +1,47 @@
<?php
namespace interfaces;
use attachments\helpers\attachment_content;
use objects\object_attachments_o;
interface attachments_i
{
/**
* List attachments for a given entity.
* @param string $type The type of the entity parent. (E.g. users_o, groups_o, etc.)
* @param int $object_id The ID of the entity.
* @param array $options Optional parameters for filtering or modifying the retrieval.
* @return object_attachments_o[] An array of attachment objects.
*/
public function list(string $type, int $object_id, array $options = []): array;
/**
* Get an attachment by its ID.
* @param int $attachment_id The ID of the attachment to be retrieved.
* @return array|null An associative array representing the attachment, or null if not found.
*/
public function get(int $attachment_id): ?object_attachments_o;
/**
* Add an attachment to a given entity.
* @param string $type The type of the entity parent. (E.g. users_o, groups_o, etc.)
* @param int $object_id The ID of the entity.
* @param attachment_content $attachment_content The content of the attachment to be added.
* @return bool True on success, false on failure.
* @see attachment_content
*/
public function create(string $type, int $object_id, attachment_content $attachment_content): bool;
/**
* Delete an attachment by its ID.
* @param int $attachment_id The ID of the attachment to be deleted.
* @return bool True on success, false on failure.
*/
public function delete(int $attachment_id): bool;
/**
* Update an existing attachment.
* @param int $attachment_id The ID of the attachment to be updated.
* @param attachment_content $attachment_content The new content for the attachment.
* @return bool True on success, false on failure.
* @see attachment_content
*/
public function update(int $attachment_id, attachment_content $attachment_content): bool;
}
@@ -0,0 +1,32 @@
<?php
namespace attachments;
require_once WD . '/modules/attachments/config/attachments_enabled_c.php';
require_once WD . '/modules/attachments/helpers/attachment_types.php';
require_once WD . '/modules/attachments/helpers/attachment_relation.php';
require_once WD . '/modules/attachments/helpers/attachment_content.php';
require_once WD . '/modules/attachments/helpers/attachment.php';
use attachments\config\attachments_enabled_c;
use traits\module_config_t;
class attachments_c
{
use module_config_t;
/**
* The status of attachments, whether it is enabled or not
* @var attachments_enabled_c
*/
public attachments_enabled_c $enabled;
public function __construct()
{
$this->setupConfig('attachments');
$this->allowUpdate([
attachments_enabled_c::class,
]);
$this->enabled = new attachments_enabled_c();
}
}
@@ -0,0 +1,29 @@
<?php
namespace attachments\config;
use Exception;
use traits\module_config_variable;
class attachments_enabled_c
{
use module_config_variable;
/**
* @throws Exception
*/
public function __construct()
{
self::setupConfigVariable(
'attachments',
'enabled',
'bool',
true,
null,
'Whether attachments are enabled',
'1',
false,
'false'
);
}
}
@@ -0,0 +1,48 @@
<?php
namespace attachments\helpers;
use objects\object_attachments_o;
class attachment
{
public ?int $id;
public ?string $object_type;
public ?int $object_id;
public attachment_content $content;
public ?string $created_at;
public ?string $updated_at;
public ?string $deleted_at;
public function __construct(?object_attachments_o $object_attachments_o = null)
{
// If no object is provided, initialize with default values
if ($object_attachments_o) {
$decodedContent = (object)json_decode($object_attachments_o->content->value(), true);
$this->populate((object)[
// Convert object_attachments_o properties to the expected types
'id' => $object_attachments_o->id,
'object_type' => $object_attachments_o->object_type->value(),
'object_id' => $object_attachments_o->object_id->value(),
'content' => $decodedContent,
'created_at' => $object_attachments_o->created_at,
'updated_at' => $object_attachments_o->updated_at,
'deleted_at' => $object_attachments_o->deleted_at,
]);
}
}
public function populate(object $object): self
{
// If the object is provided, populate the properties
if ($object) {
$this->id = isset($object->id) ? (int)$object->id : null;
$this->object_type = isset($object->object_type) ? (string)$object->object_type : null;
$this->object_id = isset($object->object_id) ? (int)$object->object_id : null;
$this->content = isset($object->content) ? new attachment_content((object)$object->content) : null;
$this->created_at = isset($object->created_at) ? (string)$object->created_at : null;
$this->updated_at = isset($object->updated_at) ? (string)$object->updated_at : null;
$this->deleted_at = isset($object->deleted_at) ? (string)$object->deleted_at : null;
}
return $this;
}
}
@@ -0,0 +1,51 @@
<?php
namespace attachments\helpers;
class attachment_content
{
public ?string $image; // Used to store the attachment object name, in the attachment store.
public ?string $document; // Used to store the attachment object name, in the attachment store.
public ?attachment_relation $relation; // Used to store the attachment relation object.
public mixed $other; // Used to store the attachment object. (if any other type)
public function __construct(?object $data = null)
{
$this->image = $data->image ?? null;
$this->document = $data->document ?? null;
$this->relation = isset($data->relation) ? new attachment_relation($data->relation) : null;
$this->other = $data->other ?? null;
}
public function toArray(): array
{
return [
'image' => $this->image,
'document' => $this->document,
'relation' => $this->relation ? $this->relation->toArray() : null,
'other' => $this->other,
];
}
/** Setters */
public function setOther(mixed $string): self
{
$this->other = $string;
return $this;
}
public function setImage(string $string): self
{
$this->image = $string;
return $this;
}
public function setDocument(string $string): self
{
$this->document = $string;
return $this;
}
public function setRelation(attachment_relation $relation): self
{
$this->relation = $relation;
return $this;
}
}
@@ -0,0 +1,27 @@
<?php
namespace attachments\helpers;
class attachment_relation
{
public string $object_type; // Type of the object (e.g., 'users_o', 'orders_o', etc.)
public int $object_id; // ID of the object
public function __construct(?object $options = null)
{
if ($options) {
foreach ($options as $key => $value) {
if (property_exists($this, $key)) {
$this->$key = $value;
}
}
}
}
public function toArray(): array
{
return [
'object_type' => $this->object_type,
'object_id' => $this->object_id,
];
}
}
@@ -0,0 +1,11 @@
<?php
namespace attachments\helpers;
enum attachment_types
{
case IMAGE; // image file
case DOCUMENT; // document file (pdf, docx, txt, etc.)
case RELATION; // relation to another object
case OTHER; // anything else
}
@@ -0,0 +1,57 @@
<?php
namespace objects;
use classes\db;
use classes\object_property;
use classes\xlvask;
use Exception;
use helpers\xlvask_customer;
use helpers\xlvask_vehicle;
use traits\db_object_t;
class object_attachments_o extends db
{
use db_object_t;
public object_property $object_type; // The object type (e.g. 'users_o', 'customers_o', etc.)
public object_property $object_id; // The ID of the object this attachment is linked to
public object_property $content; // The content of the attachment (JSON formatted)
public object_property $created_at; // The timestamp when the attachment was created
public object_property $updated_at; // The timestamp when the attachment was last updated
public object_property $deleted_at; // The timestamp when the attachment was deleted (if applicable)
public function structure(): void
{
$this->setTable('object_attachments');
}
/**
* Add a new attachment
* @param array $data The properties
* @returns void
* @throws Exception If the object was not created successfully
*/
public function add(array $data): void
{
$tmp_id = self::add_object($data);
$this->id = $tmp_id;
self::getObjectProperties();
self::objectChanged();
}
public function getObjectProperties(): void
{
$this->object_type = new object_property($this->table, $this->id, 'object_type', 'string', false);
$this->object_id = new object_property($this->table, $this->id, 'object_id', 'number', false);
$this->content = new object_property($this->table, $this->id, 'content', 'string', false);
$this->created_at = new object_property($this->table, $this->id, 'created_at', 'string', false);
$this->updated_at = new object_property($this->table, $this->id, 'updated_at', 'string', false);
$this->deleted_at = new object_property($this->table, $this->id, 'deleted_at', 'string', false);
}
public function objectChanged(): void
{
//TODO: Add cache invalidation
}
}
@@ -0,0 +1,27 @@
<?php
namespace routes;
use attachments\helpers\attachment;
use attachments\helpers\attachment_content;
use classes\attachments;
use objects\orders_o;
use traits\route_t;
class attachmentsRoute
{
use route_t;
public function run(): void
{
$this->get('/attachments/example', function () {
global $response;
throw new \Exception('EXAMPLE ROUTE, SHOULD BE IMPLEMENTED IN THE INDIVIDUAL OBJECT ROUTES');
$orders_o = new orders_o();
$orders_o->select(22636);
// Debug: Create an attachment
$orders_o->addAttachment((new attachment_content())->setOther('Hello World!'));
$response->success($orders_o->listAttachments());
});
}
}
+55
View File
@@ -2,6 +2,8 @@
namespace traits;
use attachments\helpers\attachment_content;
use classes\attachments;
use classes\db;
use classes\response;
use Exception;
@@ -29,6 +31,7 @@ use objects\logs_o;
use objects\module_action_logs_o;
use objects\motorapi_lookups_o;
use objects\notifications_o;
use objects\object_attachments_o;
use objects\order_items_o;
use objects\orders_o;
use objects\plate_scanners_o;
@@ -1144,4 +1147,56 @@ trait db_object_t
self::objectChanged();
}
}
/**
* Add an attachment to the (current) object
* @param attachment_content $content The content of the attachment
* @throws Exception If the object is not selected, it throws an exception
*/
public function addAttachment(attachment_content $content): void
{
self::requireSelected();
// Add the attachment to the object
$attachment = new attachments();
$attachment->create($this->table, $this->id, $content);
}
/**
* List attachments of the (current) object
* @return object_attachments_o[] The list of attachments of the object
* @throws Exception If the object is not selected, it throws an exception
*/
public function listAttachments(): array
{
self::requireSelected();
// List the attachments of the object
$attachment = new attachments();
return $attachment->list($this->table, $this->id);
}
/**
* Remove an attachment
* @param int $attachmentId The id of the attachment to remove
* @throws Exception If the object is not selected, it throws an exception
*/
public function removeAttachment(int $attachmentId): void
{
self::requireSelected();
// Remove the attachment from the object
$attachment = new attachments();
$attachment->delete($attachmentId);
}
/**
* Get an attachment
* @param int $attachmentId The id of the attachment to get
* @return object_attachments_o The attachment object
* @throws Exception If the object is not selected, it throws an exception
* @throws Exception If the attachment does not exist, it throws an exception
*/
public function getAttachment(int $attachmentId): object_attachments_o
{
self::requireSelected();
// Get the attachment from the object
$attachment = new attachments();
return $attachment->get($attachmentId);
}
}