1<?php 2 3namespace Doctrine\DBAL\Sharding\SQLAzure\Schema; 4 5use Doctrine\DBAL\Schema\Column; 6use Doctrine\DBAL\Schema\ForeignKeyConstraint; 7use Doctrine\DBAL\Schema\Index; 8use Doctrine\DBAL\Schema\Schema; 9use Doctrine\DBAL\Schema\Sequence; 10use Doctrine\DBAL\Schema\Table; 11use Doctrine\DBAL\Schema\Visitor\Visitor; 12use RuntimeException; 13 14use function in_array; 15 16/** 17 * Converts a single tenant schema into a multi-tenant schema for SQL Azure 18 * Federations under the following assumptions: 19 * 20 * - Every table is part of the multi-tenant application, only explicitly 21 * excluded tables are non-federated. The behavior of the tables being in 22 * global or federated database is undefined. It depends on you selecting a 23 * federation before DDL statements or not. 24 * - Every Primary key of a federated table is extended by another column 25 * 'tenant_id' with a default value of the SQLAzure function 26 * `federation_filtering_value('tenant_id')`. 27 * - You always have to work with `filtering=On` when using federations with this 28 * multi-tenant approach. 29 * - Primary keys are either using globally unique ids (GUID, Table Generator) 30 * or you explicitly add the tenant_id in every UPDATE or DELETE statement 31 * (otherwise they will affect the same-id rows from other tenants as well). 32 * SQLAzure throws errors when you try to create IDENTIY columns on federated 33 * tables. 34 */ 35class MultiTenantVisitor implements Visitor 36{ 37 /** @var string[] */ 38 private $excludedTables = []; 39 40 /** @var string */ 41 private $tenantColumnName; 42 43 /** @var string */ 44 private $tenantColumnType = 'integer'; 45 46 /** 47 * Name of the federation distribution, defaulting to the tenantColumnName 48 * if not specified. 49 * 50 * @var string 51 */ 52 private $distributionName; 53 54 /** 55 * @param string[] $excludedTables 56 * @param string $tenantColumnName 57 * @param string|null $distributionName 58 */ 59 public function __construct(array $excludedTables = [], $tenantColumnName = 'tenant_id', $distributionName = null) 60 { 61 $this->excludedTables = $excludedTables; 62 $this->tenantColumnName = $tenantColumnName; 63 $this->distributionName = $distributionName ?: $tenantColumnName; 64 } 65 66 /** 67 * {@inheritdoc} 68 */ 69 public function acceptTable(Table $table) 70 { 71 if (in_array($table->getName(), $this->excludedTables)) { 72 return; 73 } 74 75 $table->addColumn($this->tenantColumnName, $this->tenantColumnType, [ 76 'default' => "federation_filtering_value('" . $this->distributionName . "')", 77 ]); 78 79 $clusteredIndex = $this->getClusteredIndex($table); 80 81 $indexColumns = $clusteredIndex->getColumns(); 82 $indexColumns[] = $this->tenantColumnName; 83 84 if ($clusteredIndex->isPrimary()) { 85 $table->dropPrimaryKey(); 86 $table->setPrimaryKey($indexColumns); 87 } else { 88 $table->dropIndex($clusteredIndex->getName()); 89 $table->addIndex($indexColumns, $clusteredIndex->getName()); 90 $table->getIndex($clusteredIndex->getName())->addFlag('clustered'); 91 } 92 } 93 94 /** 95 * @param Table $table 96 * 97 * @return Index 98 * 99 * @throws RuntimeException 100 */ 101 private function getClusteredIndex($table) 102 { 103 foreach ($table->getIndexes() as $index) { 104 if ($index->isPrimary() && ! $index->hasFlag('nonclustered')) { 105 return $index; 106 } 107 108 if ($index->hasFlag('clustered')) { 109 return $index; 110 } 111 } 112 113 throw new RuntimeException('No clustered index found on table ' . $table->getName()); 114 } 115 116 /** 117 * {@inheritdoc} 118 */ 119 public function acceptSchema(Schema $schema) 120 { 121 } 122 123 /** 124 * {@inheritdoc} 125 */ 126 public function acceptColumn(Table $table, Column $column) 127 { 128 } 129 130 /** 131 * {@inheritdoc} 132 */ 133 public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) 134 { 135 } 136 137 /** 138 * {@inheritdoc} 139 */ 140 public function acceptIndex(Table $table, Index $index) 141 { 142 } 143 144 /** 145 * {@inheritdoc} 146 */ 147 public function acceptSequence(Sequence $sequence) 148 { 149 } 150} 151