Skip to content

Commit 198fb07

Browse files
committed
Merge remote-tracking branch 'origin/AC-14807' into spartans_pr_25062025
2 parents a20a6ff + 8279965 commit 198fb07

File tree

9 files changed

+457
-25
lines changed

9 files changed

+457
-25
lines changed

lib/internal/Magento/Framework/Setup/Declaration/Schema/UpToDateDeclarativeSchema.php

Lines changed: 283 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22
/**
3-
* Copyright © Magento, Inc. All rights reserved.
4-
* See COPYING.txt for license details.
3+
* Copyright 2018 Adobe
4+
* All Rights Reserved.
55
*/
66

77
declare(strict_types=1);
@@ -10,11 +10,12 @@
1010

1111
use Magento\Framework\Setup\Declaration\Schema\Diff\SchemaDiff;
1212
use Magento\Framework\Setup\UpToDateValidatorInterface;
13+
use Magento\Framework\Setup\DetailProviderInterface;
1314

1415
/**
1516
* Allows to validate if schema is up to date or not
1617
*/
17-
class UpToDateDeclarativeSchema implements UpToDateValidatorInterface
18+
class UpToDateDeclarativeSchema implements UpToDateValidatorInterface, DetailProviderInterface
1819
{
1920
/**
2021
* @var SchemaConfigInterface
@@ -26,6 +27,11 @@ class UpToDateDeclarativeSchema implements UpToDateValidatorInterface
2627
*/
2728
private $schemaDiff;
2829

30+
/**
31+
* @var array|null
32+
*/
33+
private $cachedDiff = null;
34+
2935
/**
3036
* UpToDateSchema constructor.
3137
* @param SchemaConfigInterface $schemaConfig
@@ -40,6 +46,8 @@ public function __construct(
4046
}
4147

4248
/**
49+
* Get the message
50+
*
4351
* @return string
4452
*/
4553
public function getNotUpToDateMessage() : string
@@ -48,13 +56,281 @@ public function getNotUpToDateMessage() : string
4856
}
4957

5058
/**
59+
* Check calculate schema differences
60+
*
5161
* @return bool
5262
*/
5363
public function isUpToDate() : bool
5464
{
55-
$declarativeSchema = $this->schemaConfig->getDeclarationConfig();
56-
$dbSchema = $this->schemaConfig->getDbConfig();
57-
$diff = $this->schemaDiff->diff($declarativeSchema, $dbSchema);
58-
return empty($diff->getAll());
65+
return empty($this->calculateDiff());
66+
}
67+
68+
/**
69+
* Get detailed information about schema differences
70+
*
71+
* @return array
72+
*/
73+
public function getDetails() : array
74+
{
75+
$diffData = $this->calculateDiff();
76+
$summary = $this->buildSummary($diffData);
77+
$summary['timestamp'] = date('Y-m-d H:i:s');
78+
79+
return $summary;
80+
}
81+
82+
/**
83+
* Calculate schema differences and cache the result
84+
*
85+
* @return array
86+
*/
87+
private function calculateDiff() : array
88+
{
89+
if ($this->cachedDiff === null) {
90+
$declarativeSchema = $this->schemaConfig->getDeclarationConfig();
91+
$dbSchema = $this->schemaConfig->getDbConfig();
92+
$diff = $this->schemaDiff->diff($declarativeSchema, $dbSchema);
93+
$this->cachedDiff = $diff->getAll() ?? [];
94+
}
95+
96+
return $this->cachedDiff;
97+
}
98+
99+
/**
100+
* Build a summary of schema differences
101+
*
102+
* @param array $diffData
103+
* @return array
104+
*/
105+
private function buildSummary(array $diffData): array
106+
{
107+
$summary = [
108+
'timestamp' => date('Y-m-d H:i:s'),
109+
'total_differences' => 0,
110+
'by_change_type' => [],
111+
'affected_tables' => [],
112+
'changes' => []
113+
];
114+
try {
115+
foreach ($diffData as $operations) {
116+
if (!is_array($operations)) {
117+
continue;
118+
}
119+
foreach ($operations as $operationType => $changes) {
120+
$this->initChangeTypeCount($summary, $operationType);
121+
122+
$changeCount = is_array($changes) ? count($changes) : 1;
123+
$summary['by_change_type'][$operationType] += $changeCount;
124+
$summary['total_differences'] += $changeCount;
125+
126+
if (!is_array($changes)) {
127+
continue;
128+
}
129+
130+
foreach ($changes as $changeIndex => $change) {
131+
$changeInfo = $this->buildChangeInfo($change, $operationType, $changeIndex, $summary);
132+
$summary['changes'][] = $changeInfo;
133+
}
134+
}
135+
}
136+
} catch (\Exception $e) {
137+
$summary['error'] = $e->getMessage();
138+
}
139+
return $summary;
140+
}
141+
142+
/**
143+
* Initialize the counter for a given operation type in the summary if not already set.
144+
*
145+
* @param array &$summary
146+
* @param string $operationType
147+
*/
148+
private function initChangeTypeCount(array &$summary, string $operationType): void
149+
{
150+
if (!isset($summary['by_change_type'][$operationType])) {
151+
$summary['by_change_type'][$operationType] = 0;
152+
}
153+
}
154+
155+
/**
156+
* Build a structured array with information about a single change operation.
157+
*
158+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
159+
* @param mixed $change
160+
* @param string $operationType
161+
* @param int|string $changeIndex
162+
* @param array $summary
163+
* @return array
164+
*/
165+
private function buildChangeInfo($change, $operationType, $changeIndex, &$summary): array
166+
{
167+
$changeInfo = [
168+
'operation' => $operationType,
169+
'index' => $changeIndex
170+
];
171+
172+
$tableName = $this->safeGetTableName($change);
173+
if ($tableName) {
174+
$changeInfo['table'] = $tableName;
175+
176+
if (!isset($summary['affected_tables'][$tableName])) {
177+
$summary['affected_tables'][$tableName] = [];
178+
}
179+
if (!isset($summary['affected_tables'][$tableName][$operationType])) {
180+
$summary['affected_tables'][$tableName][$operationType] = 0;
181+
}
182+
$summary['affected_tables'][$tableName][$operationType]++;
183+
}
184+
185+
if ($change instanceof ElementHistory) {
186+
$changeInfo = $this->processElementHistory($change, $changeInfo);
187+
} elseif (is_array($change) && isset($change['name'])) {
188+
$changeInfo['name'] = $change['name'];
189+
} elseif (is_object($change) && method_exists($change, 'getName')) {
190+
$changeInfo['name'] = $change->getName();
191+
192+
if (method_exists($change, 'getType')) {
193+
$this->isMethodExists($change, $changeInfo);
194+
}
195+
}
196+
return $changeInfo;
197+
}
198+
199+
/**
200+
* Build a structured array with method exist information.
201+
*
202+
* @param mixed $change
203+
* @param array $changeInfo
204+
*/
205+
private function isMethodExists(mixed $change, array &$changeInfo): void
206+
{
207+
$type = $change->getType();
208+
if ($type === 'index' || $type === 'constraint') {
209+
$changeInfo['type'] = $type;
210+
if (method_exists($change, 'getColumns')) {
211+
$columns = $change->getColumns();
212+
if (is_array($columns)) {
213+
$changeInfo['columns'] = array_map(function ($column) {
214+
if (is_object($column) && method_exists($column, 'getName')) {
215+
return $column->getName();
216+
}
217+
return is_string($column) ? $column : null;
218+
}, $columns);
219+
// Remove any nulls if any invalid columns found
220+
$changeInfo['columns'] = array_filter($changeInfo['columns']);
221+
}
222+
}
223+
}
224+
}
225+
226+
/**
227+
* Safely get table name from any change object
228+
*
229+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
230+
* @SuppressWarnings(PHPMD.NPathComplexity)
231+
* @param mixed $change
232+
* @return string|null
233+
*/
234+
private function safeGetTableName($change): ?string
235+
{
236+
try {
237+
// Option 1: ElementHistory with getNew() or getOld()
238+
if ($change instanceof ElementHistory) {
239+
$element = $change->getNew() ?: $change->getOld();
240+
if ($element) {
241+
// If element is a table
242+
if (method_exists($element, 'getType') && $element->getType() === 'table' &&
243+
method_exists($element, 'getName')) {
244+
return $element->getName();
245+
}
246+
247+
// If element belongs to a table
248+
if (method_exists($element, 'getTable')) {
249+
$table = $element->getTable();
250+
if ($table && method_exists($table, 'getName')) {
251+
return $table->getName();
252+
}
253+
}
254+
}
255+
}
256+
257+
// Option 2: Array with 'table' key
258+
if (is_array($change) && isset($change['table'])) {
259+
return $change['table'];
260+
}
261+
262+
// Option 3: Object with getTable() method
263+
if (is_object($change) && method_exists($change, 'getTable')) {
264+
$table = $change->getTable();
265+
if (is_string($table)) {
266+
return $table;
267+
} elseif (is_object($table) && method_exists($table, 'getName')) {
268+
return $table->getName();
269+
}
270+
}
271+
272+
// Option 4: Object is itself a table
273+
if (is_object($change) && method_exists($change, 'getType') &&
274+
$change->getType() === 'table' && method_exists($change, 'getName')) {
275+
return $change->getName();
276+
}
277+
} catch (\Exception $e) {
278+
// Silently fail and return null
279+
error_log('Error get table name: ' . $e->getMessage());
280+
}
281+
282+
return null;
283+
}
284+
285+
/**
286+
* Process ElementHistory object to extract useful information
287+
*
288+
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
289+
* @SuppressWarnings(PHPMD.NPathComplexity)
290+
* @param ElementHistory $change
291+
* @param array $changeInfo
292+
* @return array
293+
*/
294+
private function processElementHistory($change, array $changeInfo): array
295+
{
296+
try {
297+
$newElement = $change->getNew();
298+
$oldElement = $change->getOld();
299+
300+
// Get element name
301+
if ($newElement && method_exists($newElement, 'getName')) {
302+
$changeInfo['name'] = $newElement->getName();
303+
} elseif ($oldElement && method_exists($oldElement, 'getName')) {
304+
$changeInfo['name'] = $oldElement->getName();
305+
}
306+
307+
// Get element type
308+
if ($newElement && method_exists($newElement, 'getType')) {
309+
$changeInfo['type'] = $newElement->getType();
310+
} elseif ($oldElement && method_exists($oldElement, 'getType')) {
311+
$changeInfo['type'] = $oldElement->getType();
312+
}
313+
314+
// For modify operations, add basic diff information
315+
if (($changeInfo['operation'] === 'modify_column' || $changeInfo['operation'] === 'modify_table')
316+
&& $oldElement && $newElement) {
317+
// Check for comment differences (most common issue)
318+
if (method_exists($oldElement, 'getComment') && method_exists($newElement, 'getComment')) {
319+
$oldComment = $oldElement->getComment();
320+
$newComment = $newElement->getComment();
321+
322+
if ($oldComment !== $newComment) {
323+
$changeInfo['comment_changed'] = true;
324+
$changeInfo['old_comment'] = $oldComment;
325+
$changeInfo['new_comment'] = $newComment;
326+
}
327+
}
328+
}
329+
} catch (\Exception $e) {
330+
// Silently fail and return original changeInfo
331+
error_log('Error processing element history: ' . $e->getMessage());
332+
}
333+
334+
return $changeInfo;
59335
}
60336
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
/**
3+
* Copyright 2025 Adobe
4+
* All Rights Reserved.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\Setup;
9+
10+
/**
11+
* Allows to check whether specific component of database is up to date.
12+
*
13+
* New way of interaction with database implies that there can be components like:
14+
* - Declarative Schema
15+
* - Data Patches
16+
* - Schema Patches
17+
* - In Future (maybe): triggers, stored procedures, etc
18+
*
19+
* Old way implies, that each module has 2 components: data and schema
20+
*
21+
* @api
22+
*/
23+
interface DetailProviderInterface
24+
{
25+
/**
26+
* Retrieve detailed information about validator state and differences found
27+
*
28+
* @return array
29+
*/
30+
public function getDetails() : array;
31+
}

0 commit comments

Comments
 (0)