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
77declare (strict_types=1 );
1010
1111use Magento \Framework \Setup \Declaration \Schema \Diff \SchemaDiff ;
1212use 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}
0 commit comments