-
Notifications
You must be signed in to change notification settings - Fork 36
Expand file tree
/
Copy pathassertions.php
More file actions
164 lines (138 loc) · 5.66 KB
/
assertions.php
File metadata and controls
164 lines (138 loc) · 5.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
<?php
/**
* Laravel Demo Assertions
*
* Run: php examples/laravel/assertions.php
*
* These assertions verify that our assumptions about Laravel's runtime
* behaviour are correct, so the LSP can model them accurately.
* Uses only reflection (no database or app boot required).
*/
require_once __DIR__ . '/vendor/autoload.php';
// Boot Eloquent with an in-memory SQLite database
$capsule = new \Illuminate\Database\Capsule\Manager();
$capsule->addConnection([
'driver' => 'sqlite',
'database' => ':memory:',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
$passed = 0;
$failed = 0;
function check(string $label, bool $condition): void
{
global $passed, $failed;
if ($condition) {
$passed++;
} else {
$failed++;
echo "FAIL: $label\n";
}
}
function assertMethodVisibility(string $class, string $method, string $expected): void
{
$ref = new ReflectionMethod($class, $method);
$actual = $ref->isPublic() ? 'public' : ($ref->isProtected() ? 'protected' : 'private');
check("$class::$method() is $expected", $actual === $expected);
}
function assertMethodReturnType(string $class, string $method, string $expected): void
{
$ref = new ReflectionMethod($class, $method);
$type = $ref->getReturnType();
$actual = $type ? $type->__toString() : 'mixed';
check("$class::$method() returns $expected (got $actual)", $actual === $expected);
}
// ─── Scope vs Model method shadowing ────────────────────────────────────────
// Model::fresh() is public — a subclass CANNOT define a #[Scope] named "fresh"
// because PHP forbids changing the signature of an inherited public method.
// Our demo uses "freshlyBaked" instead.
check(
'Model::fresh() exists',
method_exists(\Illuminate\Database\Eloquent\Model::class, 'fresh')
);
assertMethodVisibility(\Illuminate\Database\Eloquent\Model::class, 'fresh', 'public');
// Our Bakery uses "freshlyBaked" to avoid the conflict
check(
'Bakery::freshlyBaked() exists',
method_exists(\App\Models\Bakery::class, 'freshlyBaked')
);
assertMethodVisibility(\App\Models\Bakery::class, 'freshlyBaked', 'protected');
// Verify #[Scope] attribute is present on freshlyBaked
$ref = new ReflectionMethod(\App\Models\Bakery::class, 'freshlyBaked');
$attrs = $ref->getAttributes(\Illuminate\Database\Eloquent\Attributes\Scope::class);
check('Bakery::freshlyBaked() has #[Scope] attribute', count($attrs) === 1);
// ─── Convention-based scopes ────────────────────────────────────────────────
// scopeXxx methods are public and accessible via __call as xxx()
check(
'Bakery::scopeUnbaked() exists',
method_exists(\App\Models\Bakery::class, 'scopeUnbaked')
);
assertMethodVisibility(\App\Models\Bakery::class, 'scopeUnbaked', 'public');
check(
'Bakery::scopeTopping() exists',
method_exists(\App\Models\Bakery::class, 'scopeTopping')
);
assertMethodVisibility(\App\Models\Bakery::class, 'scopeTopping', 'public');
// ─── Relationship methods ───────────────────────────────────────────────────
check(
'Bakery::baguettes() exists',
method_exists(\App\Models\Bakery::class, 'baguettes')
);
check(
'Bakery::headBaker() exists',
method_exists(\App\Models\Bakery::class, 'headBaker')
);
check(
'Bakery::masterRecipe() exists',
method_exists(\App\Models\Bakery::class, 'masterRecipe')
);
// ─── Accessor methods ───────────────────────────────────────────────────────
// Legacy accessor
check(
'Bakery::getLoafNameAttribute() exists (legacy accessor)',
method_exists(\App\Models\Bakery::class, 'getLoafNameAttribute')
);
// Modern Attribute accessor
check(
'Bakery::sprinkle() exists (modern accessor)',
method_exists(\App\Models\Bakery::class, 'sprinkle')
);
// ─── Runtime scope behaviour ────────────────────────────────────────────────
// Convention-based scopes via __call on instance return Builder
$bakery = new \App\Models\Bakery();
$result = $bakery->unbaked();
check(
'$bakery->unbaked() returns Builder via __call',
$result instanceof \Illuminate\Database\Eloquent\Builder
);
$result = $bakery->topping('choc');
check(
'$bakery->topping("choc") returns Builder via __call',
$result instanceof \Illuminate\Database\Eloquent\Builder
);
// #[Scope] attribute scopes are available on the query builder
$result = \App\Models\Bakery::query()->freshlyBaked();
check(
'Bakery::query()->freshlyBaked() returns Builder',
$result instanceof \Illuminate\Database\Eloquent\Builder
);
// Static scope forwarding
$result = \App\Models\Bakery::where('flour', 'rye');
check(
'Bakery::where() returns Builder',
$result instanceof \Illuminate\Database\Eloquent\Builder
);
// Model::fresh() on instance (non-existing model returns null)
$result = $bakery->fresh();
check(
'$bakery->fresh() returns null (Model::fresh on non-persisted)',
$result === null
);
// ─── Summary ────────────────────────────────────────────────────────────────
echo "\n";
if ($failed === 0) {
echo "\033[32m✓ All $passed assertions passed.\033[0m\n";
} else {
echo "\033[31m✗ $failed failed, $passed passed.\033[0m\n";
exit(1);
}