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