SepaHelper   A
last analyzed

Complexity

Total Complexity 21

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 21
eloc 94
dl 0
loc 232
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A isValidType() 0 6 3
A replaceSpecialChars() 0 26 2
A isTarget2Day() 0 56 4
B validString() 0 36 7
A createUID() 0 10 1
A calcDelayedDate() 0 13 4
1
<?php
2
namespace SKien\Sepa;
3
4
/**
5
 * Helper trait containing some methods used by multiple classes in package
6
 *
7
 * @package Sepa
8
 * @author Stefanius <[email protected]>
9
 * @copyright MIT License - see the LICENSE file for details
10
 * @internal
11
 */
12
trait SepaHelper
13
{
14
    /** @var array<int,array<string>>   array containing target2-dates per year     */
15
    static array $aTarget2 = [];
16
17
    /**
18
     * Check for valid type and trigger error in case of invalid type.
19
     * @param string $type
20
     * @return bool
21
     */
22
    protected function isValidType(string $type) : bool
23
    {
24
        if ($type != Sepa::CCT && $type != Sepa::CDD) {
25
            trigger_error('invalid type for ' . get_class($this), E_USER_ERROR);
26
        }
27
        return true;
28
    }
29
30
    /**
31
     * Create unique ID.
32
     * Format: 99999999-9999-9999-999999999999
33
     * @return string
34
     */
35
    public static function createUID() : string
36
    {
37
        mt_srand((int)microtime(true) * 10000);
38
        $charid = strtoupper(md5(uniqid((string)rand(), true)));
39
        $uuid = substr($charid, 0, 8) . chr(45) .
40
                substr($charid, 8, 4) . chr(45) .
41
                substr($charid, 12, 4) . chr(45) .
42
                substr($charid, 16, 12);
43
44
        return $uuid;
45
    }
46
47
    /**
48
     * Convert input to valid SEPA string.
49
     * <ol>
50
     *      <li>replacement of special chars</li>
51
     *      <li>limitation to supported chars dependend on validation type</li>
52
     *      <li>restriction to max length dependend on validation type</li>
53
     * </ol>
54
     *
55
     * Validation Types: <ul>
56
     *      <li>SepaHelper::MAX35:</li>
57
     *      <li>SepaHelper::MAX70:</li>
58
     *      <li>SepaHelper::MAX140:</li>
59
     *      <li>SepaHelper::MAX1025:
60
     *          <ul>
61
     *              <li>max length = MAX[xxx]</li>
62
     *              <li>supported chars: A...Z, a...z, 0...9, blank, dot, comma, plus, minus, slash, questionmark, colon, open/closing bracket</li>
63
     *          </ul></li>
64
     *      <li>SepaHelper::ID1:
65
     *          <ul>
66
     *              <li>max length = 35</li>
67
     *              <li>supported chars: A...Z, a...z, 0...9, blank, dot, comma, plus, minus, slash</li>
68
     *          </ul></li>
69
     *      <li>SepaHelper::ID2:
70
     *          <ul>
71
     *              <li>max length = 35</li>
72
     *              <li>supported chars: ID1 without blank</li>
73
     *          </ul></li>
74
     * </ul>
75
     *
76
     * @param string $str   string to validate
77
     * @param int $iType    type of validation: one of SepaHelper::MAX35, SepaHelper::MAX70, SepaHelper::MAX140, SepaHelper::MAX1025, SepaHelper::ID1, SepaHelper::ID2
78
     * @return string
79
     */
80
    public static function validString(string $str, int $iType) : string
81
    {
82
        // replace specialchars...
83
        $strValid = self::replaceSpecialChars($str);
84
85
        // regular expresion for 'standard' types MAXxxx
86
        $strRegEx = '/[^A-Za-z0-9 \.,\-\/\+():?]/'; // A...Z, a...z, 0...9, blank, dot, comma plus, minus, slash, questionmark, colon, open/closing bracket
87
        $strReplace = ' ';
88
        $iMaxLen = 1025;
89
        switch ($iType) {
90
            case Sepa::ID1:
91
                $iMaxLen = 35;
92
                $strRegEx = '/[^A-Za-z0-9 \.,\+\-\/]/'; // A...Z, a...z, 0...9, blank, dot, comma plus, minus, slash
93
                $strReplace = '';
94
                break;
95
            case Sepa::ID2:
96
                $iMaxLen = 35;
97
                $strRegEx = '/[^A-Za-z0-9\.,\+\-\/]/'; // same as ID1 except blank...
98
                $strReplace = '';
99
                break;
100
            case Sepa::MAX35:
101
                $iMaxLen = 35;
102
                break;
103
            case Sepa::MAX70:
104
                $iMaxLen = 70;
105
                break;
106
            case Sepa::MAX140:
107
                $iMaxLen = 140;
108
                break;
109
            case Sepa::MAX1025:
110
            default:
111
                break;
112
        }
113
114
        $strValid = preg_replace($strRegEx, $strReplace, $strValid);
115
        return substr($strValid ?? '', 0, $iMaxLen);
116
    }
117
118
    /**
119
     * Replace some special chars with nearest equivalent.
120
     * - umlauts, acute, circumflex, ...
121
     * - square/curly brackets
122
     * - underscore, at
123
     * @param string $str text to process
124
     * @return string
125
     */
126
    public static function replaceSpecialChars(string $str) : string
127
    {
128
        $strReplaced = '';
129
        if (strlen($str) > 0) {
130
            // replace known special chars
131
            $aSpecialChars = array(
132
                'á' => 'a', 'à' => 'a', 'ä' => 'ae', 'â' => 'a', 'ã' => 'a', 'å' => 'a', 'æ' => 'ae',
133
                'Á' => 'A', 'À' => 'A', 'Ä' => 'Ae', 'Â' => 'A', 'Ã' => 'A', 'Å' => 'A', 'Æ' => 'AE',
134
                'ç' => 'c', 'Ç' => 'C',
135
                'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e', 'É' => 'E', 'È' => 'E', 'Ê' => 'E', 'Ë' => 'E',
136
                'ì' => 'i', 'î' => 'i', 'ï' => 'i', 'Í' => 'I', 'Ì' => 'I', 'Î' => 'I', 'Ï' => 'I',
137
                'ñ' => 'n', 'Ñ' => 'N',
138
                'ó' => 'o', 'ò' => 'o', 'ö' => 'oe', 'ô' => 'o', 'õ' => 'o', 'ø' => 'o', 'œ' => 'oe',
139
                'Ó' => 'O', 'Ò' => 'O', 'Ö' => 'Oe', 'Ô' => 'O', 'Õ' => 'O', 'Ø' => 'O', 'Œ' => 'OE',
140
                'ß' => 'ss', 'š' => 's', 'Š' => 'S',
141
                'ú' => 'u', 'ù' => 'u', 'ü' => 'ue', 'û' => 'u',
142
                'Ú' => 'U', 'Ù' => 'U', 'Ü' => 'Ue', 'Û' => 'U',
143
                'ý' => 'y', 'ÿ' => 'y', 'Ý' => 'Y', 'Ÿ' => 'Y',
144
                'ž' => 'z', 'Ž' => 'Z',
145
                '[' => '(', ']' => ')', '{' => '(', '}' => ')',
146
                '_' => '-', '@' => '(at)', '€' => 'EUR'
147
            );
148
149
            $strReplaced = strtr($str, $aSpecialChars);
150
        }
151
        return $strReplaced;
152
    }
153
154
    /**
155
     * Calculates valid delayed date from given date considering SEPA businessdays.
156
     * @param int $iDaysDelay   min count of days the payment delays
157
     * @param int $dtRequested  requested date (unix timestamp)
158
     * @return int unix timestamp
159
     */
160
    public static function calcDelayedDate(int $iDaysDelay, int $dtRequested = 0) : int
161
    {
162
        $dtEarliest = time();
163
164
        // Annotation: should daytime ( < 08:30 / < 18:30 ) bear in mind ?
165
        $iBDays = 0;
166
        while ($iBDays < $iDaysDelay) {
167
            $dtEarliest += 86400; // add day ( 24 * 60 * 60 );
168
            if (self::isTarget2Day($dtEarliest)) {
169
                $iBDays++;
170
            }
171
        }
172
        return $dtEarliest > $dtRequested ? $dtEarliest : $dtRequested;
173
    }
174
175
    /**
176
     * Check for target2-Day (Sepa-Businessday).
177
     * Mo...Fr and NOT TARGET1-Day
178
     * TARGET1 Days:
179
     *  - New Year
180
     *  - Good Day
181
     *  - Easter Monday
182
     *  - 1'st May
183
     *  - 1'st Christmasday
184
     *  - 2'nd Christmasday
185
     * @param int $dt  unix timestamp to check
186
     * @return bool
187
     */
188
    public static function isTarget2Day(int $dt) : bool
189
    {
190
        $bTarget2Day = false;
191
        $iWeekDay = date('N', $dt);
192
        if ($iWeekDay <= 5) {
193
            // no weekend - we check for special target2 day...
194
            $iYear = intval(date('Y', $dt));
195
            if (!isset(self::$aTarget2[$iYear])) {
196
                // just generate yearly days once
197
                // first the fixed days...
198
                self::$aTarget2[$iYear] = [
199
                    $iYear . '-01-01', // New Year
200
                    $iYear . '-05-01', // 1'st May
201
                    $iYear . '-12-25', // 1'st Christmas
202
                    $iYear . '-12-26', // 2'nd Christmas
203
                ];
204
                /*
205
                 * ... and then we have the easter days dynamic to calc
206
                 * https://praxistipps.chip.de/ostern-berechnen-so-entsteht-das-datum-jedes-jahr_101993
207
                 */
208
                $a = $iYear % 4;
209
                $b = $iYear % 7;
210
                $c = $iYear % 19;
211
                $d = (19 * $c + 24) % 30;
212
                $e = (2 * $a + 4 * $b + 6 * $d + 5) % 7;
213
                $f = intdiv($c + 11 * $d + 22 * $e, 451);
214
                $iEasterMonth = 3;
215
                $iEasterSunday = 22 + $d + $e - 7 * $f;
216
                if ($iEasterSunday > 31) {
217
                    $iEasterSunday -= 31;
218
                    $iEasterMonth++;
219
                    /*
220
                     * Since the earliest possible date for Easter Sunday is March 22nd, this
221
                     * special case mentioned by the calculation rules by Gauß never occurs...
222
                     * ... and therefore cannot be covered by any test case.
223
                     *
224
                     * https://de.wikipedia.org/wiki/Osterdatum
225
                     *
226
                    if ($iEasterSunday == 26) {
227
                        $iEasterSunday = 19;
228
                        $iEasterMonth = 3;
229
                    }
230
                    */
231
                }
232
                $uxtsEasterSunnday = mktime(0, 0, 0, $iEasterMonth, $iEasterSunday, $iYear);
233
                /*
234
                 * calc friday and monday from sunday: 1day = 60sec  * 60min * 24h
235
                 * - Good Day: Easter Sunday - 2days ()
236
                 * - Easter Monday: Easter Sunday + 1day
237
                 */
238
                self::$aTarget2[$iYear][] = date('Y-m-d', $uxtsEasterSunnday - 172800);
239
                self::$aTarget2[$iYear][] = date('Y-m-d', $uxtsEasterSunnday + 86400);
240
            }
241
            $bTarget2Day = !in_array(date('Y-m-d', $dt), self::$aTarget2[$iYear]);
242
        }
243
        return $bTarget2Day;
244
        /*
245
        self::$aTarget2 = [
246
            // New Year    Good Day     Easter Monday  1'stMay         2.Christmas
247
            '2022-01-01', '2022-04-02', '2022-03-26', '2022-03-29', '2022-12-25', '2022-12-26',
248
            '2023-01-01', '2023-04-07', '2023-04-10', '2023-05-01', '2023-12-25', '2023-12-26',
249
            '2024-01-01', '2024-03-29', '2024-04-01', '2024-05-01', '2024-12-25', '2024-12-26',
250
            '2025-01-01', '2025-04-18', '2025-04-21', '2025-05-01', '2025-12-25', '2025-12-26',
251
        ];
252
        if (intval(date('Y', $dt)) >= array_pop(self::$aTarget2)) {
253
            trigger_error('No Target2 dates are specified for [' . date('Y-m-d', $dt) . '] so far!', E_USER_ERROR);
254
        }
255
        return !($iWeekDay > 5 || in_array(date('Y-m-d', $dt), self::$aTarget2));
256
        */
257
    }
258
}
259