<?php
namespace App\Controller;
use App\Form\System\EntityGeneration;
use App\Util\VaciFacilController;
use Exception;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class SystemController extends VaciFacilController
{
private const DOC_ENTITIES_PATH = "private/doctrine-entities.json";
private const DOC_ENTITIES_TEMP = "private/temp";
private const LOGGER = [
["Register", "DATETIME", false],
["LastModification", "DATETIME", true],
["WhoPerformed", "VARCHAR(50)", false],
["Version", "INT", false]
];
public const PHP = 0;
public const SQLServer = 1;
private const MAP_NAMESPACE = [
"vaciFacil" => "vfTest",
];
private const MAP_TYPES = [
"smallint" => [
self::PHP => "int",
self::SQLServer => "SMALLINT"
],
"integer" => [
self::PHP => "int",
self::SQLServer => "INT"
],
"bigint" => [
self::PHP => "string",
self::SQLServer => "BIGINT"
],
"decimal" => [
self::PHP => "float",
self::SQLServer => "DECIMAL"
],
"float" => [
self::PHP => "float",
self::SQLServer => "FLOAT"
],
"string" => [
self::PHP => "string",
self::SQLServer => [
false => "VARCHAR",
true => "CHAR"
]
],
"text" => [
self::PHP => "string",
self::SQLServer => "VARCHAR(MAX)"
],
"date" => [
self::PHP => "\Date",
self::SQLServer => "DATE"
],
"datetime" => [
self::PHP => "\DateTime",
self::SQLServer => "DATETIME"
],
"time" => [
self::PHP => "\Time",
self::SQLServer => "TIME"
],
"entity" => [
self::PHP => "mixed"
]
];
/**
* @Route(
* "/{menu}/{item}/{action}",
* methods={"GET", "POST"},
* name="system",
* requirements={"menu"="system"}
* )
* @param Request $request
* @return Response
*/
public function system(Request $request): Response
{
return $this->defaultHandler($request);
}
protected function class_generate_GET(Request $request, array $menu): Response
{
// Set the path of the doctrine entities file
$path = join("/", [$this->getParameter("kernel.project_dir"), self::DOC_ENTITIES_PATH]);
try {
// Read the doctrine entities file
$entities = json_decode(file_get_contents($path), true);
} catch (Exception) {
$entities = [];
}
// Set the form data
$formData = ["entity" => $entities];
// Set the form options
$options = $this->getFormOptions($request, [], ["data" => $formData], ["onsubmit" => "return checkForm()"]);
// Create the form object
$form = $this->defaultForm(EntityGeneration::class, $request, $options);
// Set the template used to rende the form
$template = $this->getTemplate(EntityGeneration::NAME, $menu);
// Set the form parameters
$param = $this->getTemplateParameters($form, $request, $menu, ["buttonsOnTop" => true, "entities" => $entities]);
// Return the rendered form
return $this->render($template, $param);
}
protected function class_generate_POST(Request $request, array $menu): Response
{
// Criteria used to sort the entities and properties
$ordering = fn($self, $other) => $self["order"] <=> $other["order"];
// Get the project directory
$project = $this->getParameter("kernel.project_dir");
// Process form and get the data
$this->defaultForm(EntityGeneration::class, $request, $options, $data);
// It will happen when the form is invalid
if ($data instanceof Response) return $data;
// Initialize the script content
list($drops, $script, $after) = [[], [], []];
// Sort the entities according the submitted order
uasort($data["entity"], $ordering);
// Goes through the entities marked as generate
foreach (array_filter($data["entity"], fn($item) => $item["generate"]) as $alias => $entity) {
// Check if the property was removed
if (is_null($entity["name"])) { continue; }
// Sort the properties according the submitted order
uasort($entity["properties"], $ordering);
usort($data["entity"][$alias]["properties"], $ordering);
// Check if the selected actions include the entity generation
if (in_array(1, $data["actions"])) {
try {
// Get the entity code
$class = $this->generateEntity($data["namespace"], $alias, $entity);
// Write the entity code in the temporary directory
$this->writeFile($class, $alias, "Entity", $data["namespace"]);
// Check if the user want to generate the entity for the development environment
if ($data["test"]) {
// Get the entity code
$class = $this->generateEntity($data["namespace"], $alias, $entity, true);
// Write the entity code in the temporary directory
$this->writeFile($class, $alias, "Entity", self::MAP_NAMESPACE[$data["namespace"]]);
}
// Set the operation for the log message
$operation = $this->translator->trans("inline.operation.entityGeneration");
// Add the log message to the flash bag
$msg = $this->translator->trans("success.0x009", ["%operation%" => $operation, "%object%" => $entity["name"]]);
$this->addFlash("success", $msg);
} catch (Exception $ex) {
$this->addFlash("danger", $ex->getMessage());
}
}
// Check if the selected actions include the repository generation
if (in_array(2, $data["actions"])) {
try {
// Get the repository code
$rep = $this->generateRepository($data["namespace"], $alias);
// Write the repository code in the temporary directory
$this->writeFile($rep, $alias, "Repository", $data["namespace"]);
// Check if the user want to generate the repository for the development environment
if ($data["test"]) {
// Get the repository code
$rep = $this->generateRepository(self::MAP_NAMESPACE[$data["namespace"]], $alias);
// Write the repository code in the temporary directory
$this->writeFile($rep, $alias, "Repository", self::MAP_NAMESPACE[$data["namespace"]]);
}
// Set the operation for the log message
$operation = $this->translator->trans("inline.operation.repositoryGeneration");
// Add the log message to the flash bag
$msg = $this->translator->trans("success.0x009", ["%operation%" => $operation, "%object%" => $entity["name"]]);
$this->addFlash("success", $msg);
} catch (Exception $ex) {
$this->addFlash("danger", $ex->getMessage());
}
}
// Check if the selected actions include the database script generation
if (in_array(3, $data["actions"])) {
try {
// Get the code to the current entity and append it to the script
$this->generateScript($drops, $script, $after, $alias, $entity, $data["entity"], $data["platform"]);
// Set the operation for the log message
$operation = $this->translator->trans("inline.operation.scriptGeneration");
// Add the log message to the flash bag
$msg = $this->translator->trans("success.0x009", ["%operation%" => $operation, "%object%" => $entity["name"]]);
$this->addFlash("success", $msg);
} catch (Exception $ex) {
$this->addFlash("danger", $ex->getMessage());
}
}
// Check if it needs to unmark the generation flag of the selected entities
if ($data["invert"]) {
$data["entity"][$alias]["generate"] = false;
}
}
// Check if the script have content
if (!empty($script)) {
// Used the name the script
$time = (new \DateTime())->format("Ymd");
// Set the name of the script
$fileName = "$time.sql";
// Set the path of the script
$path = join("/", [$project, self::DOC_ENTITIES_TEMP, $fileName]);
// Create the file content
$content = implode("\n", array_reverse($drops));
$content = ($content . "\n\n" . implode("\n", $script));
if (!empty($after)) {
$content = ($content . "\n" . implode("\n", $after));
}
// Write the script content to the file
file_put_contents($path, $content);
}
// Check if it needs to save the submitted information
if ($data["alterFile"]) {
foreach ($data["entity"] as $alias => $entity) {
if (is_null($entity["name"])) {
unset($data["entity"][$alias]);
continue;
}
$removed = false;
foreach ($entity["properties"] as $index => $property) {
if (is_null($property["name"])) {
unset($data["entity"][$alias]["properties"][$index]);
$removed = true;
}
}
if ($removed) {
$data["entity"][$alias]["properties"] = array_values($data["entity"][$alias]["properties"]);
}
}
// Set the path of the entities file
$path = join("/", [$project, self::DOC_ENTITIES_PATH]);
// Write the submitted data to the file
file_put_contents($path, json_encode($data["entity"], JSON_PRETTY_PRINT));
}
return $this->redirect($this->generateUrl($request->get("_route"), $menu));
}
/**
* Generates the code for the class that represent the entity
* @param string $application used to define the class namespace
* @param string $alias the entity alias (in general, have 3 letters)
* @param array $entity the entity properties with their attributes
* @param bool $development indicates if is a generation for development class
* @return string the code to be written in the file
*/
private function generateEntity(string $application, string $alias, array $entity, bool $development=false): string
{
// Set class that the new class will extend
$extends = $entity["logger"] ? "Logger" : "SimpleEntity";
// Attributes ignored in the ORM\Column annotation
$attrToIgnore = ["id", "order", "unique", "generated", "externalAlias"];
// Set the development namespace
$mappedNamespace = self::MAP_NAMESPACE[$application];
// Goes through the entity properties
foreach ($entity["properties"] as $attributes) {
// Check if the property was removed
if (is_null($attributes["type"])) { continue; }
// Get the php type of the property
$type = self::MAP_TYPES[$attributes["type"]][self::PHP];
// Get the property with the first letter in the upper case
$ucfName = ucfirst($attributes["name"]);
// Check if the column is part of a unique constraint
if ($attributes["unique"]) {
if ($attributes["type"] == "entity") {
foreach ($entity["joins"][$attributes["name"]]["columns"] as $column) {
$unique[$attributes["unique"]][] = $column["localColumnName"];
}
} else {
$unique[$attributes["unique"]][] = ($attributes["externalAlias"] ?? $alias) . ucfirst($attributes["name"]);
}
}
// Set the initial value o the ORM annotation
$aux = "\t/**\n";
// Check if the property is part of the entity id
if ($attributes["id"]) { $aux .= "\t * @ORM\Id()\n"; }
if ($attributes["generated"]) { $aux .= "\t * @ORM\GeneratedValue()\n"; }
// When the property is another entity
if ($attributes["type"] == "entity") {
// Get the columns that are part of the foreign key constraint
$columns = $entity["joins"][$attributes["name"]]["columns"];
// Set the nullable attribute as a string
$nullable = $attributes["nullable"] ? "true" : "false";
// Get the target entity of the property
$target = substr($entity["joins"][$attributes["name"]]["targetEntity"], 0, 3);
// Add the many-to-one annotation
$aux .= "\t * @ORM\\ManyToOne(targetEntity=\"$target\")\n";
// Check how many database columns are part of the foreign key
if (count($columns) == 1) {
// Set the annotation when the foreign key has one column
$aux .= "\t * @ORM\\JoinColumn(name=\"{$columns[0]["localColumnName"]}\", referencedColumnName=\"{$columns[0]["referencedColumnName"]}\", nullable=$nullable)\n";
} else {
unset($temp);
// Set the annotation when the foreign key has multiple columns
$aux .= "\t * @ORM\JoinColumns({\n";
// Goes through the columns
foreach ($columns as $column) {
// Set the annotation for the column
$temp[] = "\t\t * @ORM\\JoinColumn(name=\"{$column["localColumnName"]}\", referencedColumnName=\"{$column["referencedColumnName"]}\", nullable=$nullable)";
}
// Set the annotation in the result
$aux .= (join(",\n", $temp ?? []) . "\n");
// Set the join columns block edn
$aux .= "\t * })\n";
}
} else {
unset($attr, $columns);
// Goes through the informed attributes
foreach (array_filter($attributes) as $prop => $value) {
// Ignore attribute that aren't used by doctrine ORM
if (in_array($prop, $attrToIgnore)) { continue; }
// Set the database column name
if ($prop == "name") { $value = ($attributes["externalAlias"] ?? $alias) . ucfirst($value); }
// Convert the boolean values to string
if (is_bool($value)) { $value = $value ? "true" : "false"; }
// Attributes that need slashes
if (in_array($prop, ["name", "type"])) { $value = "\"$value\""; }
// Check if it is a collection
if (is_array($value)) {
// Used a custom function to get typed values considered as null by default
$value = array_filter($value, fn($item) => !is_null($item) && $item !== false);
// If the collection is empty, we don't need to save the property
if (empty($value)) { continue; }
// Encode the collection
$value = json_encode($value);
}
// Add the attribute to the temporary list
$attr[] = "$prop=$value";
}
// Set the ORM column attributes
$temp = join(", ", $attr ?? []);
// Add the ORM column to the result
$aux .= "\t * @ORM\Column($temp)\n";
}
// Set the property prefix
$propPrefix = $attributes["type"] != "entity" ? "?" : "";
// Set the property declaration
$properties[] = $aux . "\t */\n\tprotected $propPrefix$type \${$attributes["name"]};\n";
// Check if the property has a default value
if (!is_null($attributes["options"]) && !is_null($attributes["options"]["default"])) {
// Set the commentary part of the code
$defaults[] = "\t\t//Set";
// Set the verification and property assignment of the property in the "defaults" method
$defaults[] = "\t\tif (is_null(\$this->get$ucfName())) { \$this->set$ucfName({$attributes["options"]["default"]}); }";
}
// Ignore the code for get and set methods for the properties "code" and "link" when it is a simple class
if ($extends == "SimpleEntity" && (in_array($attributes["name"], ["code", "link"]))) { continue; }
// Set the get method for the property
$methods[] = "\tpublic function get$ucfName(): $propPrefix$type { return \$this->{$attributes["name"]}; }";
// Set the set method for the property
$methods[] = "\tpublic function set$ucfName($propPrefix$type \${$attributes["name"]}): self { \$this->{$attributes["name"]} = \${$attributes["name"]}; return \$this; }\n";
}
// Convert the properties from array to string
$properties = join("\n", $properties ?? []);
// Convert the methods from array to string
$methods = join("\n", $methods ?? []);
// Check if the entity has default values
if (isset($defaults)) {
// Convert the defaults values from array to string
$temp = join("\n", $defaults);
// Set the annotation and code for the "default" method
$defaults = "\t/**
\t * @ORM\PrePersist
\t */
\tpublic function defaults()
\t{
$temp
\t\t// Apply the remaining default
\t\tparent::defaults();
\t}\n";
} else {
// Used like this to simplify the code letter on
$defaults = "";
}
// Check for unique constraint
if (isset($unique)) {
// Remove the previous values from the variable
unset($temp);
// Goes through the columns in the constraint
foreach ($unique as $name => $values) {
// Add the quotation marks to the values
$columns = "{" . join(", ", array_map(fn($item) => "\"$item\"", $values)) . "}";
// Create the constraint annotation and add it to the list
$temp[] = " * @ORM\\UniqueConstraint(name=\"UN_{$alias}_$name\", columns=$columns)";
}
// Convert the list to a string joining the constraints
$constraints = join(",\n", $temp ?? []);
// Create the table information annotation part with the constraints
$table = " * @ORM\\Table(
* name=\"$alias{$entity["name"]}\",
* uniqueConstraints={
$constraints
* }
* )";
} else {
// Create the table information annotation part
$table = " * @ORM\\Table(name=\"$alias{$entity["name"]}\")";
}
// Create the repository annotation part
$repository = " * @ORM\\Entity(repositoryClass=\"App\\Repository\\$application\\{$alias}Repository\")";
// Create the annotation that comes before the class definition
if ($entity["logger"]) {
// When the entity have log columns
$orm = "/**
$repository
* @ORM\\EntityListeners({\"App\\Util\\WhoPerformedListener\"})
$table
* @ORM\\HasLifecycleCallbacks
*/";
} else {
// When the entity doesn't have log columns
$orm = "/**
$repository
$table
*/";
}
// Replace the production namespace for the development namespace
if ($development) { $orm = str_replace($application, $mappedNamespace, $orm); }
// Return the entity code
return $development ? "<?php
namespace App\\Entity\\$mappedNamespace;
use App\\Entity\\$application\\$alias as Official;
use Doctrine\ORM\Mapping as ORM;
$orm
class $alias extends Official {}
" : "<?php
namespace App\\Entity\\$application;
use App\\Util\\$extends;
use Doctrine\ORM\Mapping as ORM;
$orm
class $alias extends $extends
{
public const TABLE_NAME = \"$alias{$entity["name"]}\";
$properties
$methods
$defaults}
";
}
/**
* Generates the code for the class that represent the entity repository
* @param string $application used to define the class namespace
* @param string $alias the entity alias (in general, have 3 letters)
* @return string the code to be written in the file
*/
private function generateRepository(string $application, string $alias): string
{
// Generate the repository code according the default model below
return "<?php
namespace App\\Repository\\$application;
use App\\Entity\\$application\\$alias;
use Doctrine\\Bundle\\DoctrineBundle\\Repository\\ServiceEntityRepository;
use Doctrine\\Persistence\\ManagerRegistry;
/**
* @method $alias|null find(\$id, \$lockMode = null, \$lockVersion = null)
* @method $alias|null findOneBy(array \$criteria, array \$orderBy = null)
* @method {$alias}[] findAll()
* @method {$alias}[] findBy(array \$criteria, array \$orderBy = null, \$limit = null, \$offset = null)
*/
class {$alias}Repository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry \$registry)
{
parent::__construct(\$registry, $alias::class);
}
}
";
}
/**
* Generate the create table scripts for the entity
* @param array $entities the list with all entities information
* @param string $alias the entity alias (in general, have 3 letters)
* @param array $entity the entity properties with their attributes
* @param string $platform the database base platform
* @throws Exception since the type of the columns dependes on the platform, throws an exception when the type
* isn't defined in the MAP_TYPES constant for the selected platform
*/
private function generateScript(array &$drops, array &$script, array &$after, string $alias, array $entity, array $entities, string $platform)
{
$drops[] = "DROP TABLE IF EXISTS $alias{$entity["name"]};";
$result = "CREATE TABLE $alias{$entity["name"]} (\n";
if ($entity["logger"]) {
foreach (self::LOGGER as $columns) {
$column = sprintf("%-24s", $columns[0]);
$type = sprintf("%-24s", $columns[1]);
$nullable = $columns[2] ? " NULL" : "NOT NULL";
$temp[] = "\t$column$type$nullable";
}
$defaults[] = ["column" => "Register", "value" => "GETDATE()"];
$defaults[] = ["column" => "Version", "value" => "0"];
}
foreach ($entity["properties"] as $property) {
// Check if the property was removed
if (is_null($property["type"])) { continue; }
$columnName = ($property["externalAlias"] ?? $alias) . ucfirst($property["name"]);
$column = sprintf("%-24s", "$columnName");
$nullable = $property["nullable"] ? " NULL" : "NOT NULL";
if ($property["id"]) {
if ($property["type"] == "entity") {
foreach ($entity["joins"][$property["name"]]["columns"] as $column) {
$id[] = $column["localColumnName"];
}
} else {
$id[] = $columnName;
}
}
if ($property["unique"]) {
if ($property["type"] == "entity") {
foreach ($entity["joins"][$property["name"]]["columns"] as $column) {
$unique[$property["unique"]][] = $column["localColumnName"];
}
} else {
$unique[$property["unique"]][] = $columnName;
}
}
if ($property["type"] == "entity") {
$target = substr($entity["joins"][$property["name"]]["targetEntity"], 0, 3);
foreach ($entity["joins"][$property["name"]]["columns"] as $column) {
list($local, $ref) = [[], []];
$local[] = $column["localColumnName"];
$ref[] = $column["referencedColumnName"];
$search = strtolower(substr($column["referencedColumnName"], 3));
$filter = function($item) use ($search) { return $item["name"] == $search; };
$aux = array_values(array_filter($entities[$target]["properties"], $filter))[0];
$type = $this->getType($aux, $platform, true);
$temp[] = sprintf("\t%-24s%-24s%s", $column["localColumnName"], $type, $nullable);
$aux = [
"name" => "CONSTRAINT FK_{$alias}_to_$target",
"target" => $entity["joins"][$property["name"]]["targetEntity"],
"local" => join(", ", $local),
"ref" => join(", ", $ref)
];
if ($entity["order"] < $entities[$target]["order"]) {
$f = "ALTER TABLE %s%s ADD %s FOREIGN KEY (%s) REFERENCES %s (%s);";
$after[] = sprintf($f, $alias, ucfirst($entity["name"]), $aux["name"], $aux["local"], $aux["target"], $aux["ref"]);
} else {
$foreignKeys[] = $aux;
}
}
} else {
$type = sprintf("%-24s", $this->getType($property, $platform));
$temp[] = "\t$column$type$nullable";
}
if (!is_null($property["options"]) && !is_null($property["options"]["default"])) {
$value = $property["options"]["default"];
$defaults[] = [
"column" => $columnName,
"value" => is_string($value) ? "'$value'" : $value
];
}
}
if (isset($id)) {
if (count($id) == 1) {
$name = "CONSTRAINT PK_{$alias}_$id[0]";
$columns = $id[0];
} else {
$name = "CONSTRAINT PK_{$alias}_MultiColumn";
$columns = join(", ", $id);
}
$temp[] = sprintf("\t%-48sPRIMARY KEY (%s)", $name, $columns);
}
if (isset($unique)) {
foreach ($unique as $name => $values) {
$name = "CONSTRAINT UN_{$alias}_$name";
$columns = join(", ", $values);
$temp[] = sprintf("\t%-48sUNIQUE (%s)", $name, $columns);
}
}
if (isset($foreignKeys)) {
foreach ($foreignKeys as $fk) {
$f = "\t%-48sFOREIGN KEY (%s) REFERENCES %s (%s)";
$temp[] = sprintf($f, $fk["name"], $fk["local"], $fk["target"], $fk["ref"]);
}
}
if (isset($defaults)) {
foreach ($defaults as $df) {
$name = "CONSTRAINT DF_{$alias}_{$df["column"]}";
$temp[] = sprintf("\t%-48sDEFAULT %s FOR %s", $name, $df["value"], $df["column"]);
}
}
$result .= join(",\n", $temp ?? []);
$script[] = ($result . "\n);\n");
}
private function getType(array $options, int $platform, bool $foreignKey=false): string
{
// Get only the options with values
$options = array_filter($options);
// Get the doctrine type of the property
$type = $options["type"];
// Get the platform type
$mapped = self::MAP_TYPES[$type][$platform];
// Set the result
switch ($type) {
// Type that can be return direct without any detail
case "smallint":
case "integer":
case "bigint":
case "text":
case "date":
case "datetime":
case "time":
$result = $mapped;
break;
// Types that can have precision and scale
case "decimal":
case "float":
// Check if the precision and scale were informed
if (isset($options["precision"], $options["scale"])) {
// Get the precision and scale of the property
list($precision, $scale) = [$options["precision"], $options["scale"]];
// Set the result with the information
$result = "$mapped($precision, $scale)";
} else {
// No additional information provided
$result = $mapped;
}
break;
// Type that have length
case "string":
$result = "{$mapped[$options["options"]["fixed"]]}({$options["length"]})";
break;
default:
throw new Exception("Type not implement");
}
if (isset($options["generated"]) && !$foreignKey) {
$result = match ($platform) {
self::SQLServer => "$result IDENTITY(1, 1)",
default => throw new Exception("Incremental strategy not implement to the vendor \"$platform\"")
};
}
return $result;
}
private function writeFile($content, $alias, $type, $namespace)
{
// Set the project directory
$project = $this->getParameter("kernel.project_dir");
// Set the temp directory
$tempType = join("/", [$project, self::DOC_ENTITIES_TEMP, $type]);
// Set the type directory
$tempNamespace = join("/", [$project, self::DOC_ENTITIES_TEMP, $type, $namespace]);
// Create the temp directory if it doesn't exist
if (!file_exists($tempType)) { mkdir($tempType, 0775); }
// Create the type directory if it doesn't exist
if (!file_exists($tempNamespace)) { mkdir($tempNamespace, 0775); }
// Set the file name
$fileName = $type == "Entity" ? "$tempNamespace/$alias.php" : "$tempNamespace/$alias$type.php";
// Write the content in the file
file_put_contents($fileName, $content);
}
}