Source of file Silvergraph.php
Size: 12,655 Bytes - Last Modified: 2021-12-24T06:48:44+00:00
/var/www/docs.ssmods.com/process/src/code/Silvergraph.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 | <?php use SilverStripe\Core\ClassInfo; use SilverStripe\Core\Convert; use SilverStripe\Core\Environment; use SilverStripe\ORM\ArrayList; use SilverStripe\ORM\DataObject; use SilverStripe\ORM\DataObjectSchema; use SilverStripe\Core\Config\Config; use SilverStripe\Control\CliController; use SilverStripe\View\ArrayData; use SilverStripe\View\SSViewer; /** * Class Silvergraph * * Generates data model graphs from SilverSripe DataObjects, displaying database fields, relations and ancestry * * Refer to README.md for usage guide and requirements * * */ class Silvergraph extends CliController { private static $allowed_actions = array( "dot", "png", "svg" ); private function paramDefault($param, $default = null, $type = "string") { $value = $this->request->getVar($param); if (($type == "string" && empty($value)) || ($type == "numeric" && !is_numeric($value))) { $value= $default; } return $value; } /** * Generates a GraphViz dot template * * @return String a dot compatible data format */ public function dot(){ $opt = array(); $opt['location'] = $this->paramDefault('location', 'mysite'); $opt['ancestry'] = $this->paramDefault('ancestry', 1, 'numeric'); $opt['relations'] = $this->paramDefault('relations', 1, 'numeric'); $opt['fields'] = $this->paramDefault('fields', 1, 'numeric'); $opt['include_root'] = $this->paramDefault('include-root', 0, 'numeric'); $opt['exclude'] = $this->paramDefault('exclude'); $opt['group'] = $this->paramDefault('group', 0, 'numeric'); $opt['rankdir'] = $this->paramDefault('rankdir'); if (!in_array($opt['rankdir'], array('LR', 'TB', 'BT', 'RL'))) { $opt['rankdir'] = 'TB'; } $renderClasses = array(); //Get all DataObject subclasses $dataClasses = ClassInfo::subclassesFor(DataObject::class); //Remove DataObject itself array_shift($dataClasses); //Get all classes in a specific folder(s) $folders = explode(",", $opt['location']); $folderClasses = array(); foreach($folders as $folder) { if (!empty($folder)) { $folderClasses[$folder] = ClassInfo::classes_for_folder($folder); } } $excludeArray = explode(",", $opt['exclude']); //Get the intersection of the two - grouped by the folder foreach($dataClasses as $key => $dataClass) { foreach($folderClasses as $folder => $classList) { foreach($classList as $folderClass) { if (strtolower($dataClass) == strtolower($folderClass)) {; //Remove all excluded classes if (!in_array($dataClass, $excludeArray)) { $renderClasses[$folder][$dataClass] = $dataClass; } } } } } if (count($renderClasses) == 0) { user_error("No classes that extend DataObject found in location: " . Convert::raw2xml($opt['location'])); } $folders = new ArrayList(); foreach($renderClasses as $folderName => $classList) { $folder = new ArrayData(); $folder->Name = $folderName; $folder->Group = ($opt['group'] == 1); $classes = new ArrayList(); $schema = DataObject::getSchema(); foreach ($classList as $className) { $class = new ArrayData(); $class->ClassName = addslashes($className); $class->TableName = addslashes($schema->tableName($className)); //Get all the data fields for the class //fields = 0 - No fields //fields = 1 - only uninherited fields //fields = 2 - inherited fields if ($opt['fields'] > 0) { $dataFields = $schema->databaseFields($className, $opt['fields'] > 1); $class->FieldList = self::formatDataFields($dataFields); } if ($opt['relations'] > 1) { $config = Config::INHERITED; } else { $config = Config::UNINHERITED; } $hasOneArray = Config::inst()->get($className, 'has_one', $config); $hasManyArray = Config::inst()->get($className, 'has_many', $config); $manyManyArray = Config::inst()->get($className, 'many_many', $config); //TODO - what's the difference between: /* $hasOneArray = Config::inst()->get($className, 'has_one'); $hasManyArray = Config::inst()->get($className, 'has_many'); $manyManyArray = Config::inst()->get($className, 'many_many'); //and $hasOneArray = $singleton->has_one(); $hasManyArray = $singleton->has_many(); $manyManyArray = $singleton->many_many(); //Note - has_() calls are verbose - they retrieve relations all the way down to base class // ?? eg; for SiteTree, BackLinkTracking is a belongs_many_many */ //$belongsToArray = $singleton->belongs_to(); //print_r(ClassInfo::ancestry($className)); //print_r($singleton->getClassAncestry()); //Add parent class to HasOne //Remove the default "Parent" because thats the final parent, rather than the immediate parent unset($hasOneArray["Parent"]); $classAncestry = ClassInfo::ancestry($className); //getClassAncestry returns an array ordered from root to called class - to get parent, reverse and remove top element (called class) $classAncestry = array_reverse($classAncestry); array_shift($classAncestry); $parentClass = reset($classAncestry); $hasOneArray["Parent"] = $parentClass; //Ensure DataObject is not shown if include-root = 0 if ($opt['include_root'] == 0 && $parentClass == DataObject::class) { unset($hasOneArray["Parent"]); } //if ancestry = 0, remove the "Parent" relation in has_one if ($opt['ancestry'] == 0 && isset($hasOneArray["Parent"])) { unset($hasOneArray["Parent"]); } //if relations = 0, remove all but the parent relation if ($opt['relations'] == 0) { $parent = isset($hasOneArray["Parent"]) ? $hasOneArray["Parent"] : null; if ($parent) { $hasOneArray = array(); $hasOneArray["Parent"] = $parent; } else { $hasOneArray = null; } $hasManyArray = null; $manyManyArray = null; } $class->HasOneList = self::relationObject($className, $hasOneArray, $excludeArray); $class->HasManyList = self::relationObject($className, $hasManyArray, $excludeArray); $class->ManyManyList = self::relationObject($className, $manyManyArray, $excludeArray); $classes->push($class); } $folder->Classes = $classes; $folders->push($folder); } $this->customise(array( "Rankdir" => $opt['rankdir'], "Folders" => $folders )); // Defend against source_file_comments Config::nest(); Config::inst()->update(SSViewer::class, 'source_file_comments', false); // Render the output $output = $this->renderWith("Silvergraph"); // Restore the original configuration Config::unnest(); //Set output as plain text, and strip excess empty lines $this->response->addHeader("Content-type", "text/plain"); $output= preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $output); return $output; } public static function relationObject($className, $relationArray, $excludeArray) { $schema = DataObject::getSchema(); $relationList = new ArrayList(); if (is_array($relationArray)) { foreach($relationArray as $name => $remoteClass) { //Strip everything after a dot (polymorphic relations) $remoteClass = explode('.', $remoteClass)[0]; //Only add the relation if it's not in the exclusion array if (!in_array($remoteClass, $excludeArray)) { $relation = new ArrayData(); $relation->Name = $name; $relation->RemoteClass = addslashes($remoteClass); $manyMany = $schema->manyManyComponent($className, $name); if ($manyMany) { $extra = $schema->manyManyExtraFieldsForComponent($className, $name); $relation->Name = addslashes($manyMany['join']); $relation->ExtraFields = self::formatDataFields($extra); } $relationList->push($relation); } } } return $relationList; } public static function formatDataFields($dataFields) { $fields = new ArrayList(); if(!is_array($dataFields)) { return $fields; } foreach($dataFields as $fieldName => $dataType) { $field = new ArrayData(); $field->FieldName = $fieldName; //special case - Enums are too long - put new lines on commas if (strpos($dataType, "Enum") === 0) { $dataType = str_replace(",", ",\n", $dataType); } $field->DataType = $dataType; $fields->push($field); } return $fields; } /** Generate a png file from the dot template * */ public function png() { $dot = $this->dot(); $output = $this->execute("-Tpng", $dot); //Return the content as a png header('Content-type: image/png'); echo $output; } /** Generate a svg file from the dot template * */ public function svg() { $dot = $this->dot(); $output = $this->execute("-Tsvg", $dot); //Return the content as a svg header('Content-type: image/svg+xml'); echo $output; } /** Execute the dot command wih $parameters, passing in $input to stdin. Returns stdout as $output * NOTE: Requires graphviz & dot to be installed locally * (eg; apt-get install graphviz) * */ private function execute($parameters, $input) { // Prepend the path to the dot command, if explicitely defined $cmd = Environment::getEnv('SILVERGRAPH_GRAPHVIZ_PATH'); if ($cmd === false) { $cmd = ''; } $cmd .= 'dot ' . $parameters; //Execute the dot command on the local machine. //Using pipes as per the example here: http://php.net/manual/en/function.proc-open.php $descriptorspec = array( 0 => array("pipe", "r"), // stdin is a pipe that the child will read from 1 => array("pipe", "w"), // stdout is a pipe that the child will write to 2 => array("pipe", "w") // stdout is a pipe that the child will write to ); $process = proc_open($cmd, $descriptorspec, $pipes); if (is_resource($process)) { // $pipes now looks like this: // 0 => writeable handle connected to child stdin // 1 => readable handle connected to child stdout fwrite($pipes[0], $input); fclose($pipes[0]); $output = stream_get_contents($pipes[1]); $error = stream_get_contents($pipes[2]); fclose($pipes[1]); // It is important that you close any pipes before calling // proc_close in order to avoid a deadlock $return_value = proc_close($process); if (!empty($error)) { user_error("Couldn't execute dot command, ensure graphviz is installed and 'dot' postpended to your graphviz path. See README.md. Shell error: $error"); } return $output; } } } |