Skip to content

Commit 4d5a331

Browse files
committed
Fix pagination on product search page
1 parent 411e64a commit 4d5a331

30 files changed

Lines changed: 1250 additions & 147 deletions

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ All Notable changes to `bakame/stackwatch` will be documented in this file.
2222
- **BC BREAK:** `State` is renamed `Feature`
2323
- **BC BREAK:** `Input::TABLE_FORMAT` is renamed `Input::TEXT_FORMAT`
2424
- Added a `Translator` class to improve human-readable key translation.
25+
- Added `Profiler::dump` and its global function counterpart `pf` to profile, and dump a human-readable result into the output stream.
26+
- Added `Profiler::dump` and its global function counterpart `pfd` to profile, dump a human-readable result into the output stream and die.
2527

2628
### Fixed
2729

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
"autoload": {
4646
"psr-4": {
4747
"Bakame\\Stackwatch\\": "src"
48-
}
48+
},
49+
"files": [
50+
"src/functions_include.php"
51+
]
4952
},
5053
"autoload-dev": {
5154
"files": [

phpstan.neon

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,5 @@ parameters:
1010
- benchmark
1111
ignoreErrors:
1212
- identifier: offsetAccess.notFound
13-
- identifier: offsetAccess.nonOffsetAccessible
1413
reportUnmatchedIgnoredErrors: true
1514
treatPhpDocTypesAsCertain: false

src/Ansi.php

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bakame\Stackwatch;
6+
7+
use Stringable;
8+
9+
use function array_map;
10+
use function function_exists;
11+
use function getenv;
12+
use function implode;
13+
use function posix_isatty;
14+
use function preg_replace;
15+
16+
use const DIRECTORY_SEPARATOR;
17+
use const STDOUT;
18+
19+
final class Ansi
20+
{
21+
private static ?bool $enabled = null;
22+
23+
public static function enabled(): bool
24+
{
25+
if (null !== self::$enabled) {
26+
return self::$enabled;
27+
}
28+
29+
if (false !== getenv('NO_COLOR')) {
30+
return self::$enabled = false;
31+
}
32+
33+
if (false !== getenv('FORCE_COLOR')) {
34+
return self::$enabled = true;
35+
}
36+
37+
if (self::isOutputPiped()) {
38+
return self::$enabled = false;
39+
}
40+
41+
return self::$enabled = self::detectTerminalSupport();
42+
}
43+
44+
public static function disable(): void
45+
{
46+
self::$enabled = false;
47+
}
48+
49+
public static function enable(): void
50+
{
51+
self::$enabled = true;
52+
}
53+
54+
/**
55+
* Wrap text with one or multiple ANSI styles if supported.
56+
*/
57+
public static function write(Stringable|string|float|int|null $text, AnsiStyle... $styles): string
58+
{
59+
if (!self::enabled()) {
60+
return (string) $text;
61+
}
62+
63+
$prefix = implode('', array_map(fn (AnsiStyle $s) => $s->value, $styles));
64+
65+
return $prefix.$text.AnsiStyle::Reset->value;
66+
}
67+
68+
/**
69+
* Wrap text with one or multiple ANSI styles if supported and finish with a newline character.
70+
*/
71+
public static function writeln(Stringable|string|float|int|null $text, AnsiStyle... $styles): string
72+
{
73+
return self::write($text."\n", ...$styles);
74+
}
75+
76+
private static function isOutputPiped(): bool
77+
{
78+
if (function_exists('posix_isatty')) {
79+
return !posix_isatty(STDOUT);
80+
}
81+
82+
return DIRECTORY_SEPARATOR === '\\' ? (false === getenv('TERM')) : false;
83+
}
84+
85+
private static function detectTerminalSupport(): bool
86+
{
87+
if (DIRECTORY_SEPARATOR === '\\') {
88+
return false !== getenv('ANSICON')
89+
|| 'ON' === getenv('ConEmuANSI')
90+
|| 'xterm' === getenv('TERM')
91+
|| (function_exists('sapi_windows_vt100_support') && Warning::cloak(sapi_windows_vt100_support(...), STDOUT)); /* @phpstan-ignore-line */
92+
}
93+
94+
return false !== getenv('TERM')
95+
&& 'dumb' !== getenv('TERM');
96+
}
97+
98+
public static function error(Stringable|string|float|int|null $text): string
99+
{
100+
return self::write($text, AnsiStyle::Red, AnsiStyle::Bold);
101+
}
102+
103+
public static function success(Stringable|string|float|int|null $text): string
104+
{
105+
return self::write($text, AnsiStyle::Green, AnsiStyle::Bold);
106+
}
107+
108+
public static function warning(Stringable|string|float|int|null $text): string
109+
{
110+
return self::write($text, AnsiStyle::Yellow, AnsiStyle::Bold);
111+
}
112+
113+
public static function info(Stringable|string|float|int|null $text): string
114+
{
115+
return self::write($text, AnsiStyle::Cyan);
116+
}
117+
118+
public static function stripStyle(string $text): string
119+
{
120+
return preg_replace('/\033\[[0-9;]*m/', '', $text) ?? $text;
121+
}
122+
}

src/AnsiStyle.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bakame\Stackwatch;
6+
7+
enum AnsiStyle: string
8+
{
9+
// Text effects
10+
case Reset = "\033[0m";
11+
case Bold = "\033[1m";
12+
case Dim = "\033[2m";
13+
case Underline = "\033[4m";
14+
case Blink = "\033[5m";
15+
case Reverse = "\033[7m";
16+
17+
// Foreground colors
18+
case Black = "\033[30m";
19+
case Red = "\033[31m";
20+
case Green = "\033[32m";
21+
case Yellow = "\033[33m";
22+
case Blue = "\033[34m";
23+
case Magenta = "\033[35m";
24+
case Cyan = "\033[36m";
25+
case White = "\033[37m";
26+
27+
// Background colors
28+
case BlackBg = "\033[40m";
29+
case RedBg = "\033[41m";
30+
case GreenBg = "\033[42m";
31+
case YellowBg = "\033[43m";
32+
case BlueBg = "\033[44m";
33+
case MagentaBg = "\033[45m";
34+
case CyanBg = "\033[46m";
35+
case WhiteBg = "\033[47m";
36+
}

src/AnsiTest.php

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Bakame\Stackwatch;
6+
7+
use PHPUnit\Framework\Attributes\CoversClass;
8+
use PHPUnit\Framework\TestCase;
9+
use ReflectionClass;
10+
use ReflectionException;
11+
12+
#[CoversClass(Ansi::class)]
13+
#[CoversClass(AnsiStyle::class)]
14+
final class AnsiTest extends TestCase
15+
{
16+
protected function setUp(): void
17+
{
18+
// Reset static state before each test
19+
$ref = new ReflectionClass(Ansi::class);
20+
$prop = $ref->getProperty('enabled');
21+
$prop->setValue(null, null);
22+
23+
// Clear environment variables we might manipulate
24+
putenv('NO_COLOR');
25+
putenv('FORCE_COLOR');
26+
putenv('TERM');
27+
putenv('ANSICON');
28+
putenv('ConEmuANSI');
29+
}
30+
31+
public function testEnableAndDisable(): void
32+
{
33+
Ansi::disable();
34+
self::assertFalse(Ansi::enabled());
35+
36+
Ansi::enable();
37+
self::assertTrue(Ansi::enabled());
38+
}
39+
40+
public function testNoColorEnvironmentVariableDisables(): void
41+
{
42+
putenv('NO_COLOR=1');
43+
self::assertFalse(Ansi::enabled());
44+
}
45+
46+
public function testForceColorEnvironmentVariableEnables(): void
47+
{
48+
putenv('FORCE_COLOR=1');
49+
self::assertTrue(Ansi::enabled());
50+
}
51+
52+
public function testWriteWithoutAnsi(): void
53+
{
54+
Ansi::disable();
55+
self::assertSame('Hello', Ansi::write('Hello', AnsiStyle::Bold));
56+
}
57+
58+
public function testWriteWithAnsi(): void
59+
{
60+
Ansi::enable();
61+
$expected = AnsiStyle::Bold->value.'Hello'.AnsiStyle::Reset->value;
62+
self::assertSame($expected, Ansi::write('Hello', AnsiStyle::Bold));
63+
}
64+
65+
public function testWritelnAddsNewline(): void
66+
{
67+
Ansi::enable();
68+
$expected = AnsiStyle::Underline->value."Hello\n".AnsiStyle::Reset->value;
69+
self::assertSame($expected, Ansi::writeln('Hello', AnsiStyle::Underline));
70+
}
71+
72+
public function testErrorMessageStyling(): void
73+
{
74+
Ansi::enable();
75+
$expected = AnsiStyle::Red->value.AnsiStyle::Bold->value.'Error'.AnsiStyle::Reset->value;
76+
self::assertSame($expected, Ansi::error('Error'));
77+
}
78+
79+
public function testSuccessMessageStyling(): void
80+
{
81+
Ansi::enable();
82+
$expected = AnsiStyle::Green->value.AnsiStyle::Bold->value.'Ok'.AnsiStyle::Reset->value;
83+
self::assertSame($expected, Ansi::success('Ok'));
84+
}
85+
86+
public function testWarningMessageStyling(): void
87+
{
88+
Ansi::enable();
89+
$expected = AnsiStyle::Yellow->value.AnsiStyle::Bold->value.'Warn'.AnsiStyle::Reset->value;
90+
self::assertSame($expected, Ansi::warning('Warn'));
91+
}
92+
93+
public function testInfoMessageStyling(): void
94+
{
95+
Ansi::enable();
96+
$expected = AnsiStyle::Cyan->value.'Info'.AnsiStyle::Reset->value;
97+
self::assertSame($expected, Ansi::info('Info'));
98+
}
99+
100+
public function testStripStyleRemovesAllAnsiCodes(): void
101+
{
102+
$styled = AnsiStyle::Red->value.AnsiStyle::Bold->value.'Hello'.AnsiStyle::Reset->value;
103+
self::assertSame('Hello', Ansi::stripStyle($styled));
104+
}
105+
106+
public function testStripStyleWithoutAnsiIsUnchanged(): void
107+
{
108+
self::assertSame('PlainText', Ansi::stripStyle('PlainText'));
109+
}
110+
111+
public function testDetectTerminalSupportOnUnix(): void
112+
{
113+
// Simulate TERM set
114+
putenv('TERM=xterm');
115+
self::assertTrue($this->invokePrivateMethod(Ansi::class, 'detectTerminalSupport'));
116+
117+
// Simulate TERM=dumb
118+
putenv('TERM=dumb');
119+
self::assertFalse($this->invokePrivateMethod(Ansi::class, 'detectTerminalSupport'));
120+
121+
// TERM unset
122+
putenv('TERM');
123+
self::assertFalse($this->invokePrivateMethod(Ansi::class, 'detectTerminalSupport'));
124+
}
125+
126+
/**
127+
* @param class-string $class
128+
* @param array<mixed> $args
129+
*
130+
* @throws ReflectionException
131+
*/
132+
private function invokePrivateMethod(string $class, string $method, array $args = []): mixed
133+
{
134+
$ref = new ReflectionClass($class);
135+
$m = $ref->getMethod($method);
136+
137+
return $m->invokeArgs(null, $args);
138+
}
139+
}

0 commit comments

Comments
 (0)