• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

bleedingdeacons / confur / 31015112238

05 Aug 2026 02:25PM UTC coverage: 95.194% (-0.1%) from 95.299%
31015112238

push

github

web-flow
chore(tooling): make PSR-12 a gate and badge the standard (#50)

* chore(tooling): exclude WordPress and test-only sniffs from PSR-12

Groundwork for making PHPCS a gate. No formatting changes here - this commit
only narrows the ruleset, adds the .gitattributes this repo was missing, and
promotes the CI step, so the reformatting that follows is reviewable on its
own.

* style: reindent with spaces and give five methods a visibility keyword

This plugin was tab-indented throughout while the rest of the suite is not,
which accounted for 2,116 of its findings across 18 files - more than a
quarter of the whole suite's. phpcbf did that reindent plus the usual
structural tidying: opening braces onto their own line, inline
if (...) { return x; } expanded, multi-line call arguments aligned,
trailing blank lines before closing braces dropped.

Not purely whitespace, so git diff -w on this commit is not empty. The
non-whitespace part is the brace and inline-control-structure restructuring
above, plus five methods that were declared with no visibility keyword and
a snake_case name:

  AcfHelper::update_acf_field   -> public static function updateAcfField
  AcfHelper::update_acf_fields  -> public static function updateAcfFields
  AcfHelper::update_acf_field2  -> public static function updateAcfField2
  AnswerShortcode::get_field    -> public function getField
  AnswerShortcode::get_the_ID   -> public function getTheId

The AcfHelper three are called only from their own tests and one commented
line in AnswerHandler; both were updated. The AnswerShortcode two are thin
wrappers that exist so the global ACF/WP calls can be swapped out in tests -
they kept their // Calls global comments, and their two internal callers
in the same class were updated. No behaviour changes.

Two file-level docblocks in GeneralShortcodes and HtmlHelper sat between a
class_exists() guard and the class they describe, which PSR-12 reads as a
misplaced file docblock. Each moved inside its gua... (continued)

1022 of 1111 new or added lines in 21 files covered. (91.99%)

3 existing lines in 3 files now uncovered.

2258 of 2372 relevant lines covered (95.19%)

4.08 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.64
/src/Shortcodes/GeneralShortcodes.php
1
<?php
2

3
namespace Confur\Shortcodes;
4

5
use Confur\Utils\HtmlHelper;
6
use DateTime;
7
use DateTimeZone;
8
use Throwable;
9

UNCOV
10
if (!class_exists(__NAMESPACE__ . '\\GeneralShortcodes')) {
×
11
    /**
12
     * General purpose shortcodes
13
     *
14
     * Both Confur and Amber ship this class. The guard around it ensures
15
     * we don't re-declare the Confur\Shortcodes\GeneralShortcodes class
16
     * if (somehow) this file is included twice. The two plugins' copies
17
     * live in different namespaces so they never collide with each other.
18
     */
19
    class GeneralShortcodes
20
    {
21
        /**
22
         * Open link in new tab
23
         *
24
         * @param array<string, mixed> $atts Shortcode attributes
25
         * @param string|null $content Shortcode content
26
         * @return string Rendered HTML
27
         */
28
        public function openBlank(array $atts = [], ?string $content = null): string
2 ✔
29
        {
30
            try {
31
                $atts = shortcode_atts([
2 ✔
32
                    'href' => '#',
2 ✔
33
                    'class' => ''
2 ✔
34
                ], $atts);
2 ✔
35

36
                return HtmlHelper::createLink(
2 ✔
37
                    esc_attr($atts['href']),
2 ✔
38
                    esc_attr($atts['class']),
2 ✔
39
                    $content
2 ✔
40
                );
2 ✔
41
            } catch (Throwable $e) {
1 ✔
42
                return sprintf(
1 ✔
43
                    '[openBlank error: %s | href="%s", class="%s"]',
1 ✔
44
                    $e->getMessage(),
1 ✔
45
                    $atts['href'] ?? '',
1 ✔
46
                    $atts['class'] ?? ''
1 ✔
47
                );
1 ✔
48
            }
49
        }
50

51
        /**
52
         * Create email link
53
         *
54
         * @param array<string, mixed> $atts Shortcode attributes
55
         * @param string|null $content Shortcode content
56
         * @return string Rendered HTML
57
         */
58
        public function linkEmail(array $atts = [], ?string $content = null): string
3 ✔
59
        {
60
            try {
61
                $atts = shortcode_atts([
3 ✔
62
                    'address' => '',
3 ✔
63
                    'subject' => null
3 ✔
64
                ], $atts);
3 ✔
65

66
                if (empty($atts['address'])) {
3 ✔
67
                    return $content ?? '';
1 ✔
68
                }
69

70
                return HtmlHelper::createEmailAnchor(
2 ✔
71
                    $atts['address'],
2 ✔
72
                    $atts['subject'],
2 ✔
73
                    $content
2 ✔
74
                );
2 ✔
75
            } catch (Throwable $e) {
1 ✔
76
                return sprintf(
1 ✔
77
                    '[linkEmail error: %s | address="%s", subject="%s"]',
1 ✔
78
                    $e->getMessage(),
1 ✔
79
                    $atts['address'] ?? '',
1 ✔
80
                    $atts['subject'] ?? ''
1 ✔
81
                );
1 ✔
82
            }
83
        }
84

85
        /**
86
         * Generate PDF link
87
         *
88
         * @param array<string, mixed> $atts Shortcode attributes
89
         * @param string|null $content Shortcode content
90
         * @return string Rendered HTML
91
         */
92
        public function generatePdfLink(array $atts = [], ?string $content = null): string
3 ✔
93
        {
94
            try {
95
                $atts = shortcode_atts([
3 ✔
96
                    'url' => '',
3 ✔
97
                    'name' => ''
3 ✔
98
                ], $atts);
3 ✔
99

100
                if (empty($atts['url']) || empty($atts['name'])) {
3 ✔
101
                    return '[generatePdfLink error: Missing required parameters | url="' . ($atts['url'] ?? '') . '", name="' . ($atts['name'] ?? '') . '"]';
1 ✔
102
                }
103

104
                return '<div>' . HtmlHelper::generatePdfLink(
2 ✔
105
                    $atts['url'],
2 ✔
106
                    $atts['name'],
2 ✔
107
                    $content
2 ✔
108
                ) . '</div>';
2 ✔
109
            } catch (Throwable $e) {
1 ✔
110
                return sprintf(
1 ✔
111
                    '[generatePdfLink error: %s | url="%s", name="%s"]',
1 ✔
112
                    $e->getMessage(),
1 ✔
113
                    $atts['url'] ?? '',
1 ✔
114
                    $atts['name'] ?? ''
1 ✔
115
                );
1 ✔
116
            }
117
        }
118

119
        /**
120
         * Generate days remaining
121
         *
122
         * @param array<string, mixed> $atts Shortcode attributes
123
         * @return string Rendered HTML
124
         */
125
        public function generateDaysRemaining(array $atts = []): string
7 ✔
126
        {
127
            try {
128
                $atts = shortcode_atts([
7 ✔
129
                    'end_date' => '',
7 ✔
130
                    'extend_by' => 0
7 ✔
131
                ], $atts);
7 ✔
132

133
                if (empty($atts['end_date'])) {
7 ✔
134
                    return 'Please provide an end date.';
1 ✔
135
                }
136

137
                $timezone = $this->getTimezone();
6 ✔
138
                $endDateString = trim($atts['end_date']);
6 ✔
139
                $endDateObj = $this->parseDate($endDateString, $timezone);
5 ✔
140

141
                if ($endDateObj === null) {
5 ✔
142
                    return 'Invalid date format. Use "YYYY-MM-DD" or "YYYY-MM-DD HH:MM" (24-hour format).';
1 ✔
143
                }
144

145
                $extendBy = max(0, (int)$atts['extend_by']);
4 ✔
146
                if ($extendBy > 0) {
4 ✔
147
                    $endDateObj->modify("+{$extendBy} days");
1 ✔
148
                }
149

150
                $formattedDate = $this->formatDate($endDateObj);
4 ✔
151
                $currentTime = new DateTime('now', $timezone);
4 ✔
152
                $diffSeconds = $endDateObj->getTimestamp() - $currentTime->getTimestamp();
4 ✔
153

154
                if ($diffSeconds < 0) {
4 ✔
155
                    return 'The date has already passed.';
1 ✔
156
                }
157

158
                $timeRemainingText = $this->formatTimeRemaining($diffSeconds);
3 ✔
159
                $extensionText = $this->formatExtensionText($extendBy);
3 ✔
160

161
                return "<strong>Deadline:</strong> $formattedDate$extensionText - $timeRemainingText";
3 ✔
162
            } catch (Throwable $e) {
1 ✔
163
                return sprintf(
1 ✔
164
                    '[generateDaysRemaining error: %s | end_date="%s", extend_by="%s"]',
1 ✔
165
                    $e->getMessage(),
1 ✔
166
                    $atts['end_date'] ?? '',
1 ✔
167
                    $atts['extend_by'] ?? ''
1 ✔
168
                );
1 ✔
169
            }
170
        }
171

172
        /**
173
         * Get the WordPress timezone or fallback to UTC
174
         *
175
         * @return DateTimeZone
176
         */
177
        private function getTimezone(): DateTimeZone
6 ✔
178
        {
179
            try {
180
                if (function_exists('wp_timezone')) {
6 ✔
181
                    return wp_timezone();
6 ✔
182
                }
183
            } catch (Throwable) {
×
184
                // Fall through to UTC default
185
            }
186

187
            return new DateTimeZone('UTC');
×
188
        }
189

190
        /**
191
         * Parse a date string into a DateTime object
192
         *
193
         * @param string $dateString The date string to parse
194
         * @param DateTimeZone $timezone The timezone to use
195
         * @return DateTime|null The parsed DateTime or null on failure
196
         */
197
        private function parseDate(string $dateString, DateTimeZone $timezone): ?DateTime
5 ✔
198
        {
199
            $formats = strpos($dateString, ':') !== false
5 ✔
200
                ? ['Y-m-d H:i', 'Y-m-d H:i:s']
1 ✔
201
                : ['Y-m-d'];
4 ✔
202

203
            foreach ($formats as $format) {
5 ✔
204
                $dateObj = DateTime::createFromFormat($format, $dateString, $timezone);
5 ✔
205
                if ($dateObj !== false) {
5 ✔
206
                    return $dateObj;
4 ✔
207
                }
208
            }
209

210
            // Fallback: try generic parsing
211
            try {
212
                return new DateTime($dateString, $timezone);
1 ✔
213
            } catch (Throwable) {
1 ✔
214
                return null;
1 ✔
215
            }
216
        }
217

218
        /**
219
         * Format a DateTime object for display
220
         *
221
         * @param DateTime $date The date to format
222
         * @return string The formatted date string
223
         */
224
        private function formatDate(DateTime $date): string
4 ✔
225
        {
226
            if ($date->format('H:i:s') === '00:00:00') {
4 ✔
227
                return $date->format('d/m/Y');
×
228
            }
229

230
            return $date->format('d/m/Y H:i');
4 ✔
231
        }
232

233
        /**
234
         * Format the time remaining text
235
         *
236
         * @param int $diffSeconds Seconds remaining
237
         * @return string Formatted time remaining
238
         */
239
        private function formatTimeRemaining(int $diffSeconds): string
3 ✔
240
        {
241
            if ($diffSeconds < 24 * 60 * 60) {
3 ✔
242
                $hoursRemaining = max(1, (int)ceil($diffSeconds / 3600));
1 ✔
243
                $unit = $hoursRemaining === 1 ? 'hour' : 'hours';
1 ✔
244
                return "<strong>$hoursRemaining</strong> $unit remaining.";
1 ✔
245
            }
246

247
            $daysRemaining = (int)ceil($diffSeconds / (60 * 60 * 24));
2 ✔
248
            $unit = $daysRemaining === 1 ? 'day' : 'days';
2 ✔
249
            return "<strong>$daysRemaining</strong> $unit remaining.";
2 ✔
250
        }
251

252
        /**
253
         * Format the extension text
254
         *
255
         * @param int $extendBy Number of days extended
256
         * @return string Extension text or empty string
257
         */
258
        private function formatExtensionText(int $extendBy): string
3 ✔
259
        {
260
            if ($extendBy <= 0) {
3 ✔
261
                return '';
2 ✔
262
            }
263

264
            $unit = $extendBy === 1 ? 'day' : 'days';
1 ✔
265
            return " (extended by $extendBy $unit)";
1 ✔
266
        }
267
    }
268
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc