Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Error
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
6 / 6
8
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 make
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 generic
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 __toString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Eco;
6
7use Eco\Contracts\ErrorCodeContract;
8use Eco\Enums\ErrorCode;
9
10/**
11 * Represents an error carried by a failed {@see Result}.
12 *
13 * Immutable by design — every property is read-only and set only once
14 * at construction time. Use the semantic factory methods instead of
15 * calling the constructor directly.
16 *
17 * The $code property accepts any {@see ErrorCodeContract} implementation,
18 * so you can bring your own domain-specific enum:
19 *
20 * ```php
21 * enum AppErrorCode: string implements ErrorCodeContract
22 * {
23 *     case UNAUTHORIZED = 'UNAUTHORIZED';
24 *     case INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE';
25 *
26 *     public function value(): string { return $this->value; }
27 * }
28 *
29 * // Then use it directly:
30 * Error::make(AppErrorCode::UNAUTHORIZED, 'Access denied.')
31 * Error::make(AppErrorCode::INSUFFICIENT_BALANCE, 'Not enough credits.', 'balance')
32 * ```
33 */
34final class Error
35{
36    /**
37     * @param ErrorCodeContract $code    Machine-readable identifier.
38     *                                   Use {@see ErrorCode} for built-in codes or
39     *                                   your own enum implementing {@see ErrorCodeContract}.
40     * @param string            $message Human-readable description of what went wrong.
41     * @param string            $field   The input field that caused the error, if applicable.
42     *                                   Useful for mapping validation errors back to form fields.
43     */
44    public function __construct(
45        public readonly ErrorCodeContract $code,
46        public readonly string            $message,
47        public readonly string            $field = '',
48    ) {}
49
50    /**
51     * Creates an error with any code implementing {@see ErrorCodeContract}.
52     *
53     * This is the universal factory — use it for domain-specific codes
54     * that go beyond the built-in {@see ErrorCode} cases.
55     *
56     * ```php
57     * Error::make(AppErrorCode::UNAUTHORIZED,        'Access denied.')
58     * Error::make(AppErrorCode::INSUFFICIENT_BALANCE, 'Not enough credits.', 'balance')
59     * ```
60     */
61    public static function make(ErrorCodeContract $code, string $message, string $field = ''): self
62    {
63        return new self($code, $message, $field);
64    }
65
66    /**
67     * Creates a generic, unclassified error.
68     * Prefer {@see make()} with a specific code when the error type is known.
69     */
70    public static function generic(string $message): self
71    {
72        return new self(ErrorCode::GENERIC, $message);
73    }
74
75    /**
76     * Creates a validation error tied to a specific input field.
77     *
78     * Use when user-provided data does not meet format or constraint rules
79     * (e.g. empty required field, invalid e-mail, value out of range).
80     *
81     * ```php
82     * Error::validation('email', 'Must be a valid e-mail address.')
83     * Error::validation('age',   'Must be at least 18.')
84     * ```
85     */
86    public static function validation(string $field, string $message): self
87    {
88        return new self(ErrorCode::VALIDATION, $message, $field);
89    }
90
91    /**
92     * Returns a structured array representation of this error.
93     *
94     * Empty values are filtered out so the output stays clean for JSON responses.
95     *
96     * ```php
97     * ['code' => 'VALIDATION_ERROR', 'message' => '...', 'field' => 'email']
98     * ```
99     */
100    public function toArray(): array
101    {
102        return array_filter([
103            'code'    => $this->code->value(),
104            'message' => $this->message,
105            'field'   => $this->field ?: null,
106        ]);
107    }
108
109    /**
110     * Returns a human-readable string representation.
111     * Prefixes the message with the field name when present.
112     *
113     * Example output:
114     *  - `[email] Must be a valid e-mail address. (VALIDATION_ERROR)`
115     *  - `Access denied. (UNAUTHORIZED)`
116     */
117    public function __toString(): string
118    {
119        $prefix = $this->field ? "[{$this->field}" : '';
120
121        return "{$prefix}{$this->message} ({$this->code->value()})";
122    }
123}