fix: PSR-4 compliance — rename core/middleware/services to PascalCase for Linux server compatibility
This commit is contained in:
70
app/Core/AI.php
Normal file
70
app/Core/AI.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use App\Services\InvoiceExtractionService;
|
||||
|
||||
/**
|
||||
* Gemini AI Integration for Invoice Extraction
|
||||
* Optimized for Jordan UBL 2.1 Compliance
|
||||
*/
|
||||
class AI
|
||||
{
|
||||
private static string $baseUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent";
|
||||
|
||||
/**
|
||||
* Extract Data from Invoice Image or PDF (Base64)
|
||||
*/
|
||||
public static function extractInvoiceData(string $base64Data, string $mimeType): ?array
|
||||
{
|
||||
$apiKey = env('GEMINI_API_KEY');
|
||||
if (!$apiKey) {
|
||||
error_log('AI Error: GEMINI_API_KEY is missing');
|
||||
return null;
|
||||
}
|
||||
|
||||
$service = new InvoiceExtractionService();
|
||||
$prompt = $service->buildExtractionPrompt();
|
||||
|
||||
$payload = [
|
||||
"contents" => [
|
||||
[
|
||||
"parts" => [
|
||||
["text" => $prompt],
|
||||
[
|
||||
"inline_data" => [
|
||||
"mime_type" => $mimeType,
|
||||
"data" => $base64Data
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
"generationConfig" => [
|
||||
"response_mime_type" => "application/json"
|
||||
]
|
||||
];
|
||||
|
||||
$ch = curl_init(self::$baseUrl . "?key=" . $apiKey);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
error_log("AI Error: Gemini API returned code $httpCode. Response: " . $response);
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
$textResponse = $result['candidates'][0]['content']['parts'][0]['text'] ?? null;
|
||||
|
||||
if (!$textResponse) return null;
|
||||
|
||||
return json_decode($textResponse, true);
|
||||
}
|
||||
}
|
||||
51
app/Core/Database.php
Normal file
51
app/Core/Database.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple PDO Database Wrapper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
use PDO;
|
||||
use PDOException;
|
||||
|
||||
final class Database
|
||||
{
|
||||
private static ?PDO $instance = null;
|
||||
|
||||
public static function getInstance(): PDO
|
||||
{
|
||||
if (self::$instance === null) {
|
||||
$config = require APP_PATH . '/config/database.php';
|
||||
|
||||
$dsn = sprintf(
|
||||
"mysql:host=%s;port=%s;dbname=%s;charset=%s",
|
||||
$config['host'],
|
||||
$config['port'],
|
||||
$config['database'],
|
||||
$config['charset']
|
||||
);
|
||||
|
||||
try {
|
||||
self::$instance = new PDO($dsn, $config['username'], $config['password'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
||||
PDO::ATTR_EMULATE_PREPARES => false,
|
||||
]);
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode(['success' => false, 'message' => 'Database connection failed']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function generateUuid(): string
|
||||
{
|
||||
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4));
|
||||
}
|
||||
}
|
||||
81
app/Core/Encryption.php
Normal file
81
app/Core/Encryption.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* Advanced Encryption (AES-256-GCM) - System Level
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Encryption
|
||||
{
|
||||
private const CIPHER = 'aes-256-gcm';
|
||||
|
||||
/**
|
||||
* Encrypts data using the system's ENCRYPTION_KEY from .env
|
||||
*/
|
||||
public static function encrypt(string $data): string
|
||||
{
|
||||
$key = env('ENCRYPTION_KEY');
|
||||
if (!$key) {
|
||||
throw new \RuntimeException('ENCRYPTION_KEY is missing from .env');
|
||||
}
|
||||
|
||||
$encryptionKey = hash('sha256', $key, true);
|
||||
$iv = random_bytes(openssl_cipher_iv_length(self::CIPHER));
|
||||
|
||||
$tag = '';
|
||||
$ciphertext = openssl_encrypt($data, self::CIPHER, $encryptionKey, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
|
||||
if ($ciphertext === false) {
|
||||
throw new \RuntimeException('Encryption failed');
|
||||
}
|
||||
|
||||
return base64_encode($iv . $tag . $ciphertext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts AES-256-GCM encrypted data using the system's ENCRYPTION_KEY
|
||||
*/
|
||||
public static function decrypt(string $encryptedData): string|false
|
||||
{
|
||||
$key = env('ENCRYPTION_KEY');
|
||||
if (!$key) {
|
||||
throw new \RuntimeException('ENCRYPTION_KEY is missing from .env');
|
||||
}
|
||||
|
||||
// Handle common prefixing issues or trailing whitespace
|
||||
$encryptedData = trim($encryptedData);
|
||||
if (str_starts_with($encryptedData, '==')) {
|
||||
$encryptedData = substr($encryptedData, 2);
|
||||
}
|
||||
|
||||
$encryptionKey = hash('sha256', $key, true);
|
||||
$decoded = base64_decode($encryptedData, true);
|
||||
|
||||
if ($decoded === false) {
|
||||
error_log("ENCRYPTION ERROR: Invalid base64 data provided for decryption.");
|
||||
return false;
|
||||
}
|
||||
|
||||
$ivLength = openssl_cipher_iv_length(self::CIPHER);
|
||||
$tagLength = 16;
|
||||
|
||||
if (strlen($decoded) < $ivLength + $tagLength) {
|
||||
// This is likely legacy unencrypted data, return false silently
|
||||
return false;
|
||||
}
|
||||
|
||||
$iv = substr($decoded, 0, $ivLength);
|
||||
$tag = substr($decoded, $ivLength, $tagLength);
|
||||
$ciphertext = substr($decoded, $ivLength + $tagLength);
|
||||
|
||||
$result = openssl_decrypt($ciphertext, self::CIPHER, $encryptionKey, OPENSSL_RAW_DATA, $iv, $tag);
|
||||
|
||||
if ($result === false) {
|
||||
error_log("ENCRYPTION ERROR: openssl_decrypt failed. Key might be wrong or data corrupted.");
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
52
app/Core/JWT.php
Normal file
52
app/Core/JWT.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple JWT (HMAC SHA256)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class JWT
|
||||
{
|
||||
private static function base64UrlEncode(string $data): string
|
||||
{
|
||||
return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($data));
|
||||
}
|
||||
|
||||
private static function base64UrlDecode(string $data): string
|
||||
{
|
||||
return base64_decode(str_replace(['-', '_'], ['+', '/'], $data));
|
||||
}
|
||||
|
||||
public static function encode(array $payload, string $secret): string
|
||||
{
|
||||
$header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']);
|
||||
$base64UrlHeader = self::base64UrlEncode($header);
|
||||
$base64UrlPayload = self::base64UrlEncode(json_encode($payload));
|
||||
|
||||
$signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true);
|
||||
$base64UrlSignature = self::base64UrlEncode($signature);
|
||||
|
||||
return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature;
|
||||
}
|
||||
|
||||
public static function decode(string $token, string $secret): ?array
|
||||
{
|
||||
$parts = explode('.', $token);
|
||||
if (count($parts) !== 3) return null;
|
||||
|
||||
[$header, $payload, $signature] = $parts;
|
||||
|
||||
$expectedSignature = self::base64UrlEncode(hash_hmac('sha256', $header . "." . $payload, $secret, true));
|
||||
|
||||
if (!hash_equals($expectedSignature, $signature)) return null;
|
||||
|
||||
$decodedPayload = json_decode(self::base64UrlDecode($payload), true);
|
||||
|
||||
// Check expiry
|
||||
if (isset($decodedPayload['exp']) && $decodedPayload['exp'] < time()) return null;
|
||||
|
||||
return $decodedPayload;
|
||||
}
|
||||
}
|
||||
213
app/Core/JoFotara.php
Normal file
213
app/Core/JoFotara.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
/**
|
||||
* JoFotara (Jordan E-Invoicing) Integration Core
|
||||
* Handles UBL 2.1 XML Generation, Cryptography, and API Communication
|
||||
*/
|
||||
class JoFotara
|
||||
{
|
||||
private string $baseUrl = 'https://backend.jofotara.gov.jo/core/invoices/';
|
||||
|
||||
/**
|
||||
* 1. Generate UBL 2.1 XML for an invoice
|
||||
*/
|
||||
public function generateXML(array $invoice, array $company): string
|
||||
{
|
||||
$issueDate = $invoice['invoice_date'] ?? date('Y-m-d');
|
||||
$issueTime = date('H:i:s');
|
||||
$typeCode = $invoice['ubl_type_code'] ?? '388';
|
||||
$category = $invoice['invoice_category'] ?? 'simplified';
|
||||
|
||||
// Prepare data outside heredoc for clean interpolation
|
||||
$buyerName = $this->xmlEscape($invoice['buyer_name'] ?: 'عميل نقدي');
|
||||
$buyerId = $invoice['buyer_tin'] ?: $invoice['buyer_national_id'] ?: '000000000';
|
||||
$payMethod = $invoice['payment_method_code'] ?: '013';
|
||||
$supplierName = $this->xmlEscape($company['name']);
|
||||
$supplierAddress = $this->xmlEscape($company['address'] ?? '');
|
||||
|
||||
$xml = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
|
||||
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:CustomizationID>urn:www.cen.eu:en16931:2017#compliant#urn:www.josefotara.jo:trns:ubl:3.0</cbc:CustomizationID>
|
||||
<cbc:ProfileID>reporting:1.0</cbc:ProfileID>
|
||||
<cbc:ID>{$invoice['invoice_number']}</cbc:ID>
|
||||
<cbc:IssueDate>{$issueDate}</cbc:IssueDate>
|
||||
<cbc:IssueTime>{$issueTime}</cbc:IssueTime>
|
||||
<cbc:InvoiceTypeCode name="{$category}">{$typeCode}</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>JOD</cbc:DocumentCurrencyCode>
|
||||
<cbc:TaxCurrencyCode>JOD</cbc:TaxCurrencyCode>
|
||||
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName><cbc:Name>{$supplierName}</cbc:Name></cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>{$supplierAddress}</cbc:StreetName>
|
||||
<cac:Country><cbc:IdentificationCode>JO</cbc:IdentificationCode></cac:Country>
|
||||
</cac:PostalAddress>
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>{$company['tax_identification_number']}</cbc:CompanyID>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
<cac:PartyLegalEntity>
|
||||
<cbc:RegistrationName>{$supplierName}</cbc:RegistrationName>
|
||||
</cac:PartyLegalEntity>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName><cbc:Name>{$buyerName}</cbc:Name></cac:PartyName>
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>{$buyerId}</cbc:CompanyID>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
|
||||
<cac:PaymentMeans>
|
||||
<cbc:PaymentMeansCode>{$payMethod}</cbc:PaymentMeansCode>
|
||||
</cac:PaymentMeans>
|
||||
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($invoice['tax_amount'])}</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="JOD">{$this->fmt($invoice['subtotal'] - $invoice['discount_total'])}</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($invoice['tax_amount'])}</cbc:TaxAmount>
|
||||
<cac:TaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>16.000</cbc:Percent>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:TaxCategory>
|
||||
</cac:TaxSubtotal>
|
||||
</cac:TaxTotal>
|
||||
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="JOD">{$this->fmt($invoice['subtotal'])}</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="JOD">{$this->fmt($invoice['subtotal'] - $invoice['discount_total'])}</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="JOD">{$this->fmt($invoice['grand_total'])}</cbc:TaxInclusiveAmount>
|
||||
<cbc:AllowanceTotalAmount currencyID="JOD">{$this->fmt($invoice['discount_total'])}</cbc:AllowanceTotalAmount>
|
||||
<cbc:PayableAmount currencyID="JOD">{$this->fmt($invoice['grand_total'])}</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
|
||||
{$this->buildInvoiceLines($invoice['items'])}
|
||||
</Invoice>
|
||||
XML;
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function buildInvoiceLines(array $items): string
|
||||
{
|
||||
$result = '';
|
||||
foreach ($items as $item) {
|
||||
$taxAmount = round($item['line_total'] * $item['tax_rate'], 3);
|
||||
$taxCategory = $item['tax_rate'] > 0 ? 'S' : 'Z';
|
||||
|
||||
$result .= <<<XML
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>{$item['line_number']}</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="PCE">{$this->fmt($item['quantity'])}</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="JOD">{$this->fmt($item['line_total'])}</cbc:LineExtensionAmount>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($taxAmount)}</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="JOD">{$this->fmt($item['line_total'])}</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($taxAmount)}</cbc:TaxAmount>
|
||||
<cac:TaxCategory>
|
||||
<cbc:ID>{$taxCategory}</cbc:ID>
|
||||
<cbc:Percent>{$this->fmt($item['tax_rate'] * 100)}</cbc:Percent>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:TaxCategory>
|
||||
</cac:TaxSubtotal>
|
||||
</cac:TaxTotal>
|
||||
<cac:Item>
|
||||
<cbc:Description>{$this->xmlEscape($item['description'])}</cbc:Description>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount currencyID="JOD">{$this->fmt($item['unit_price'])}</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
XML;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function fmt(float $val): string { return number_format($val, 3, '.', ''); }
|
||||
private function xmlEscape(string $str): string { return htmlspecialchars($str, ENT_XML1, 'UTF-8'); }
|
||||
|
||||
/**
|
||||
* 2. Generate Base64 TLV QR Code (Local Fallback)
|
||||
*/
|
||||
public function generateQRCode(array $invoiceData): string
|
||||
{
|
||||
$sellerName = $invoiceData['supplier_name'] ?? '';
|
||||
$taxNumber = $invoiceData['supplier_tin'] ?? '';
|
||||
$timestamp = date('Y-m-d\TH:i:s\Z', strtotime($invoiceData['invoice_date'] ?? 'now'));
|
||||
$total = number_format($invoiceData['grand_total'] ?? 0, 3, '.', '');
|
||||
$vat = number_format($invoiceData['tax_amount'] ?? 0, 3, '.', '');
|
||||
|
||||
$tlv = $this->toTLV(1, $sellerName) .
|
||||
$this->toTLV(2, $taxNumber) .
|
||||
$this->toTLV(3, $timestamp) .
|
||||
$this->toTLV(4, $total) .
|
||||
$this->toTLV(5, $vat);
|
||||
|
||||
return base64_encode($tlv);
|
||||
}
|
||||
|
||||
private function toTLV(int $tag, string $value): string
|
||||
{
|
||||
return chr($tag) . chr(strlen($value)) . $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. Submit Invoice to JoFotara API
|
||||
*/
|
||||
public function submitInvoice(string $xmlContent, string $clientId, string $secretKey): array
|
||||
{
|
||||
// For production, we must encode XML in Base64 and wrap in JSON
|
||||
$payload = json_encode([
|
||||
'invoice' => base64_encode($xmlContent)
|
||||
]);
|
||||
|
||||
$ch = curl_init($this->baseUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
"ClientId: $clientId",
|
||||
"SecretKey: $secretKey"
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decoded = json_decode($response, true) ?? [];
|
||||
$decoded['_http_code'] = $httpCode;
|
||||
|
||||
if ($httpCode === 200) {
|
||||
return [
|
||||
'success' => true,
|
||||
'uuid' => $decoded['invoiceUUID'] ?? $decoded['uuid'] ?? 'mock-' . uniqid(),
|
||||
'qrCode' => $decoded['qrCode'] ?? $decoded['QRCode'] ?? null,
|
||||
'raw' => $decoded
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $decoded['errorMessage'] ?? 'API Connection Failed',
|
||||
'raw' => $decoded
|
||||
];
|
||||
}
|
||||
}
|
||||
42
app/Core/Security.php
Normal file
42
app/Core/Security.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple Security Helpers
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Security
|
||||
{
|
||||
/**
|
||||
* Recursively sanitize input data (strings and arrays)
|
||||
*/
|
||||
public static function sanitize($data)
|
||||
{
|
||||
if (is_array($data)) {
|
||||
foreach ($data as $key => $value) {
|
||||
$data[$key] = self::sanitize($value);
|
||||
}
|
||||
} else if (is_string($data)) {
|
||||
$data = htmlspecialchars(strip_tags(trim($data)), ENT_QUOTES, 'UTF-8');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
public static function generateRandomString(int $length = 64): string
|
||||
{
|
||||
return bin2hex(random_bytes($length / 2));
|
||||
}
|
||||
|
||||
public static function sign(string $data, string $secret): string
|
||||
{
|
||||
return hash_hmac('sha256', $data, $secret);
|
||||
}
|
||||
|
||||
public static function verifySignature(string $data, string $signature, string $secret): bool
|
||||
{
|
||||
$expected = self::sign($data, $secret);
|
||||
return hash_equals($expected, $signature);
|
||||
}
|
||||
}
|
||||
25
app/Core/Validator.php
Normal file
25
app/Core/Validator.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Simple Data Validator
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Core;
|
||||
|
||||
final class Validator
|
||||
{
|
||||
public static function validate(array $data, array $rules): array
|
||||
{
|
||||
$errors = [];
|
||||
foreach ($rules as $field => $rule) {
|
||||
if (str_contains($rule, 'required') && (empty($data[$field]) && $data[$field] !== '0')) {
|
||||
$errors[$field] = "The {$field} field is required.";
|
||||
}
|
||||
if (str_contains($rule, 'email') && !empty($data[$field]) && !filter_var($data[$field], FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$field] = "The {$field} must be a valid email address.";
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user