view src/Helpers/RelationshipNavigator.php @ 24:31109c61ce02 codex

Refactor RelationshipNavigator formatting and implement belongsTo, hasMany, belongsToMany injection in ModelGenerator
author Luka Sitas <sitas.luka.97@gmail.com>
date Tue, 22 Apr 2025 21:37:44 -0400
parents 827efbf4d73c
children
line wrap: on
line source

<?php

namespace Wizard\MagicForger\Helpers;

use Illuminate\Support\Facades\DB;

/**
 * Class RelationshipNavigator
 *
 * This class is responsible for navigating database table relationships, specifically 
 * identifying and categorizing 'belongsTo', 'hasMany', and 'hasManyThrough' relationships.
 */
class RelationshipNavigator
{
    /**
     * Handles the retrieval and display of table relationships.
     *
     * @return void
     */
    public static function handle()
    {
        $tables = DB::select('SHOW TABLES');
        $tableNames = array_map(fn($table) => current((array) $table), $tables);

        foreach ($tableNames as $table) {
            echo "Table: $table \n";

            $relations = self::getRelations($table);
            echo "Relationships: \n";

            foreach ($relations as $relation => $relatedTables) {
                echo "$relation: \n";
                foreach ($relatedTables as $relatedTable) {
                    echo "\t";
                    foreach ($relatedTable as $key => $value) {
                        if (is_array($value)) {
                            echo "\n\t\t" . implode("\n\t\t", array_map(fn($k, $v) => "$k: $v", array_keys($value), $value));
                        } else {
                            echo "$key: $value ";
                        }
                    }
                    echo "\n";
                }
            }
            echo "\n --- \n";
        }
    }

    /**
     * Retrieves relationships of a specific table.
     *
     * @param string $table The table name.
     * @return array An array containing 'belongsTo', 'hasMany', and 'hasManyThrough' relations.
     */
    public static function getRelations($table)
    {
        $relations = [
            'belongsTo' => [],
            'hasMany' => [],
            'hasManyThrough' => [],
        ];

        $foreignKeys = DB::select("SHOW KEYS FROM $table WHERE Key_name != 'PRIMARY'");
        $referencedTables = self::getAllReferencedTables($table);

        // Determine 'belongsTo' Relationships
        foreach ($foreignKeys as $fk) {
            $column = $fk->Column_name;

            // Skip certain columns
            if (in_array($column, ['created_by', 'updated_by'])) {
                continue;
            }

            $referencedTable = $referencedTables[$column] ?? null;

            if ($referencedTable) {
                $relations['belongsTo'][] = ['column' => $column, 'table' => $referencedTable->REFERENCED_TABLE_NAME];
            }
        }

        // Determine 'hasMany' Relationships
        if ($reverseRelation = self::findReverseRelation($table)) {
            foreach ($reverseRelation as $relatedTable) {
                $relations['hasMany'][] = $relatedTable;
            }
        }

        // Determine 'hasManyThrough' Relationships
        if ($hasManyThroughRelations = self::findHasManyThroughRelations($table)) {
            foreach ($hasManyThroughRelations as $relatedTable) {
                $relations['hasManyThrough'][] = $relatedTable;
            }
        }

        return $relations;
    }

    /**
     * Retrieves all referenced tables for a given table.
     *
     * @param string $table The table name.
     * @return array|null An associative array of referenced tables, keyed by column name.
     */
    public static function getAllReferencedTables($table)
    {
        $results = DB::select("
            SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME, REFERENCED_TABLE_NAME, REFERENCED_COLUMN_NAME 
            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
            WHERE TABLE_SCHEMA = DATABASE() 
            AND REFERENCED_TABLE_NAME IS NOT NULL
            AND TABLE_NAME = ?
        ", [$table]);

        return self::re_key_array($results, 'COLUMN_NAME') ?: null;
    }

    /**
     * Finds 'hasMany' inverse relationships for a given table.
     *
     * @param string $table The table name.
     * @return array|null An array of related tables with column names.
     */
    public static function findReverseRelation($table)
    {
        $relations = DB::select("
            SELECT TABLE_NAME, COLUMN_NAME
            FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE 
            WHERE TABLE_SCHEMA = DATABASE() 
            AND REFERENCED_TABLE_NAME = ? 
            AND REFERENCED_COLUMN_NAME = 'id'
            AND COLUMN_NAME NOT IN ('created_by', 'updated_by')
        ", [$table]);

        return array_map(fn($rel) => ['table' => $rel->TABLE_NAME, 'column' => $rel->COLUMN_NAME], $relations) ?: null;
    }
    
    /**
     * Finds 'hasManyThrough' relationships for a given table.
     *
     * @param string $table The table name.
     * @return array An array of 'hasManyThrough' relationships.
     */
    public static function findHasManyThroughRelations($table)
    {
        $relations = [];
        $intermediaryTables = self::findReverseRelation($table);

        if ($intermediaryTables !== null) {
            foreach ($intermediaryTables as $intermediary) {
                if ($isPivot = self::isPivot($intermediary['table'])) {
                    $isPivot = current($isPivot);

                    $potentialTables = array_keys($isPivot['tables']);
                    $externalTable = $potentialTables[0] === $table ? $isPivot['tables'][$potentialTables[1]] : $isPivot['tables'][$potentialTables[0]];
                    $internalTable = $potentialTables[0] === $table ? $isPivot['tables'][$potentialTables[0]] : $isPivot['tables'][$potentialTables[1]];

                    $hasManyThrough = [
                        'table' => $externalTable['table_name'],
                        'through' => [
                            'table' => $isPivot['table'],
                            'external_column' => $externalTable['column'],
                            'internal_column' => $internalTable['column'],
                        ],
                    ];
                    $relations[] = $hasManyThrough;
                }
            }
        }

        return $relations;
    }

    /**
     * Determines if a table is a pivot table.
     *
     * @param string $table The table name.
     * @return array|null An array with pivot details or null if not a pivot.
     */
    public static function isPivot($table)
    {
        $relations = [];
        $pivotTables = DB::select("
            SELECT TABLE_NAME, TABLE_COMMENT
            FROM INFORMATION_SCHEMA.TABLES 
            WHERE TABLE_SCHEMA = DATABASE() 
            AND TABLE_NAME = ?
            AND TABLE_COMMENT != ''
        ", [$table]);

        if (!is_null($pivotTables) && count($pivotTables) > 0) {
            $ref = current($pivotTables);
            $pivots = json_decode(str_replace('PIVOT:', '', $ref->TABLE_COMMENT), true);
            $tables = [];

            if (count($pivots) > 0) {
                $references = self::getAllReferencedTables($table);
                $references = self::re_key_array($references, 'REFERENCED_TABLE_NAME');

                foreach ($pivots as $key => $value) {
                    if ($refData = ($references[$value] ?? null)) {
                        $tables[$value] = [
                            'table_name' => $value,
                            'column' => $refData->COLUMN_NAME,
                        ];
                    }
                }
            }
            $relations[] = ['table' => $ref->TABLE_NAME, 'tables' => $tables];
        }

        return !empty($relations) ? $relations : null;
    }

    /**
     * Re-keys an array of objects using a specific object's property.
     *
     * @param array $oldArray The original array of objects.
     * @param string $key The key to re-index by.
     * @return array The re-keyed array.
     */
    public static function re_key_array($oldArray, $key)
    {
        $newArray = [];
        if (count($oldArray) > 0) {
            foreach ($oldArray as $array) {
                $newArray[$array->$key] = $array;
            }
        }
        return $newArray;
    }
}