{"id":338,"date":"2022-07-15T09:01:27","date_gmt":"2022-07-15T07:01:27","guid":{"rendered":"https:\/\/carsten.familie-schumann.info\/blog\/?p=338"},"modified":"2023-02-10T14:02:28","modified_gmt":"2023-02-10T12:02:28","slug":"ms-sql-mit-symfony-unter-linux-nutzen","status":"publish","type":"post","link":"https:\/\/carsten.familie-schumann.info\/blog\/2022\/07\/ms-sql-mit-symfony-unter-linux-nutzen\/","title":{"rendered":"MS-SQL mit Symfony unter Linux nutzen"},"content":{"rendered":"<div data-post-id=\"307\" class=\"insert-page insert-page-307 post-entry-content\"><p>In manchen F\u00e4llen m\u00f6chte man von seiner Symfony-Applikation, die auf einem Linux-Server l\u00e4uft, auf einen Microsoft SQL-Server zugreifen. In meinem konkreten Anwendungsfall lief die eigentliche Symfony-Applikation auf einem aktuellen Debian mit Doctrine auf MariaDB und sollte aus einem anderen System zus\u00e4tzliche Informationen holen. Dieses System besa\u00df weder eine nutzbare API noch einfach auswertbare Webseiten\/JSON-Antworten sodass nur der unsch\u00f6ne direkte Weg \u00fcber die Datenbank ging.<\/p>\n<h2>Als Entwickler ist man meist naiv&#8230;<\/h2>\n<p>&#8230;denn man geht davon aus, dass ein so weit verbreiteter Server auch eine vern\u00fcnftige Unterst\u00fctzung hat. Grade jetzt, wo Microsoft in Windows 10\/11 ein Linux-Subsystem einsetzt und sich Open Source ganz gro\u00df auf die Fahne schreibt. Erste Recherchen zeigten, dass Microsoft zu Zeiten von PHP 5.5 mal einen Datenbanktreiber erstellt hatte. Doctrine kann diesen auch mit dem Datenbanktypen <code>mssql<\/code> (alias <code>pdo_sqlsrv<\/code>) verwenden.<\/p>\n<p>Sucht man nun den <code>pdo_sqlsrv<\/code>, landet man unter <a href=\"https:\/\/www.php.net\/manual\/de\/ref.pdo-sqlsrv.php\">Microsoft SQL Server Functions (PDO_SQLSRV) auf php.net<\/a> auf der Installstionsseite, die einen direkt ersteinmal aufkl\u00e4rt<\/p>\n<blockquote><p>The PDO_SQLSRV extension is only compatible with PHP running on Windows.<\/p><\/blockquote>\n<p>Leider wurder der Treiber nach PHP5.5 (Juni 2013) nur noch unter Windows weiterentwickelt. Darunter wird jedoch auf eine Alternative verwiesen. Den <a href=\"https:\/\/www.microsoft.com\/en-us\/download\/details.aspx?id=28160\">Microsoft ODBC-Treiber f\u00fcr Linux<\/a>&#8230;.zuletzt aktualisiert 2012. Man kann sich schon denken, wohin das f\u00fchrt, ein Blick in die README best\u00e4tigt dies aber:<\/p>\n<blockquote><p>This driver is only supported on 64-bit Red Hat Enterprise Linux 5 or 64-bit Red Hat Enterprise Linux 6. Other configurations are not supported and will not likely work.<\/p><\/blockquote>\n<p>OK, kein Support durch Microsoft&#8230;.<\/p>\n<p>Im Netz existieren noch einige Anleitungen, wie man mod_mssql Als letzte M\u00f6glichkeit kann noch der mod_mssql_so elbst compiliert werden.<br \/>\nhttps:\/\/www.activelamp.com\/blog\/development\/mssql-symfony<\/p>\n<h2>Die Community kommt zur Hilfe<\/h2>\n<p>Wie es h\u00e4ufig bei propriet\u00e4ren Microsoft-L\u00f6sungen ist, findet die Linux-Community eine L\u00f6sung. In diesem Fall war Brian Bruns zur Jahrtausendwende mit <a href=\"https:\/\/www.freetds.org\">FreeTDS<\/a>.<\/p>\n<p>Die Installation unter Debian gestaltet sich denkbar einfach:<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\r\nsudo apt install freetds-common freetds-bin unixodbc tdsodbc\r\nsudo apt install php-sybase    \r\n<\/pre>\n<p>Die zweite Zeile wird sp\u00e4ter ben\u00f6tigt, um von PHP auf FreeTDS zuzugreifen. Zuerst wird aber der ODBC-Treiber getestet. FreeTDS kann entweder per Konfigurationsdatei (<code>\/etc\/freetds\/freetds.conf<\/code> Konfiguriert werden oder bei dem Aufruf mit allen notwendigen Parametern versorgt werden. Da die Datenbankverbindung sp\u00e4ter durch die Web-Applikation konfiguriert werden soll, wird die letztere Variante verwendet und man muss keine Anpassungen an der Konfigurationsdatei machen.<\/p>\n<p>Testen kann man den ODBC-Treiber \u00fcber das Kommandozeilentool <code>tsql<\/code>:<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\r\n#tsql -H &lt;hostname&gt; -p &lt;port&gt; -U &lt;username&gt; -P &lt;password&gt; -D &lt;database&gt;\r\ntsql -H sqlsrv.example.com -p 1443 -U test -P geheim -D testdatabase\r\n<\/pre>\n<p>Ein erfolgreiche Verbindung sieht etwa so aus:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n    locale is &quot;de_DE.UTF-8&quot;\r\n    locale charset is &quot;UTF-8&quot;\r\n    using default charset &quot;UTF-8&quot;\r\n    Setting lb as default database in login packet\r\n    1&gt; quit\r\n<\/pre>\n<h2>Jetzt &#8222;nur&#8220; noch in Doctrine einbinden<\/h2>\n<p>Damit dachte ich, w\u00e4re der Drops gelutscht und ich k\u00f6nnte zum interessanten Teil \u00fcbergehen&#8230;.doch weit gefehlt. Denn wie sage ich nun Doctrine, dass es die ODBC-Verbindung verwenden soll? Definiert man bei Doctrine die Platform <code>sqlsrv<\/code>, m\u00f6chte Doctrine immer noch <code>pdo_sqlsrv<\/code> verwenden.<\/p>\n<p>Im Netz geistern einige Composer-Module oder Symfony-Bundles herum, welche dieses Problem l\u00f6sen woll(t)en. Beispielhaft seien hier genannt:<\/p>\n<ul>\n<li><a href=\"https:\/\/github.com\/realestateconz\/MssqlBundle\">realestateconz\/MssqlBundle<\/a> (letzte Aktualisierung 2018)<\/li>\n<li><a href=\"https:\/\/github.com\/mediamonks\/symfony-mssql-bundle\">mediamonks\/symfony-mssql-bundle<\/a> (letzte Aktualisierung 2017)<\/li>\n<li><a href=\"https:\/\/packagist.org\/packages\/leaseweb\/doctrine-pdo-dblib\">leaseweb\/doctrine-pdo-dblib<\/a> (abadoned without replacement)<\/li>\n<\/ul>\n<p>Kurzum: Sie l\u00f6sen das Problem nicht mehr. Meist wird in den Bundles ein kleinerer oder gr\u00f6\u00dferer Teil von Doctrine ge\u00e4ndert. Da jedoch in Doctrine einige Klassen <code>private<\/code> oder <code>final<\/code> sind\/waren, mussten die Autoren der Packages den Weg gehen und legitime Doctrine-Klassen duplizieren. Diese Code-Duplizierung setzt jedoch vorraus, dass jegliche \u00c4nderungen in den Funktionssignaturen nachgepflegt werden, was bei allen von mir gefundenen Packages nicht der Fall ist. Ich habe bei einem der Pakete einmal angefangen, die Signaturen entsprechend anzupassen, bin aber schnell zum Punkt gekommen, wo die generelle Klassenstruktur h\u00e4tte ver\u00e4ndert werden m\u00fcssen.<\/p>\n<h2>Ein neuer Treiber<\/h2>\n<p>Generell stellt sich sowieso die Frage, warum (fast) alles neu definiert werden muss, wenn doch nur das Transport-Layer ge\u00e4ndert werden soll (wir diskutieren ja \u00fcber PDO). Hier kommt man schnell zum Ergebnis, dass eigentlich nur die Art, wie die PDO-Connection aufgebaut wird, anders ist und alles andere eigentlich schon vorhanden ist. Also schnell einen neuen Driver geschrieben (bzw. aus Doctrine kopiert und an den relevanten Stellen ge\u00e4ndert; Vererbung geht wegen final-Schl\u00fcsselwort nicht):<\/p>\n<pre class=\"brush: php; title: ; notranslate\" title=\"\">\r\n&lt;?php\r\n\/\/ File: src\/Lib\/OdbcMssql\/Driver.php\r\n\r\nnamespace App\\Lib\\OdbcMssql;\r\n\r\nuse Doctrine\\DBAL\\Driver\\AbstractSQLServerDriver;\r\nuse Doctrine\\DBAL\\Driver\\AbstractSQLServerDriver\\Exception\\PortWithoutHost;\r\nuse Doctrine\\DBAL\\Driver\\Exception;\r\nuse Doctrine\\DBAL\\Driver\\PDO\\Connection as PDOConnection;\r\nuse Doctrine\\DBAL\\Driver\\PDO\\Exception as PDOException;\r\nuse Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection;\r\nuse function is_int;\r\nuse PDO;\r\nuse function sprintf;\r\n\r\nfinal class Driver extends AbstractSQLServerDriver\r\n{\r\n    \/**\r\n     * {@inheritdoc}\r\n     *\r\n     * @return Connection\r\n     *\/\r\n    public function connect(array $params)\r\n    {\r\n        $driverOptions = $dsnOptions = &#x5B;];\r\n\r\n        if (isset($params&#x5B;'driverOptions'])) {\r\n            foreach ($params&#x5B;'driverOptions'] as $option =&gt; $value) {\r\n                if (is_int($option)) {\r\n                    $driverOptions&#x5B;$option] = $value;\r\n                } else {\r\n                    $dsnOptions&#x5B;$option] = $value;\r\n                }\r\n            }\r\n        }\r\n\r\n        if (!empty($params&#x5B;'persistent'])) {\r\n            $driverOptions&#x5B;PDO::ATTR_PERSISTENT] = true;\r\n        }\r\n\r\n        try {\r\n            $pdo = new PDO(\r\n                $this-&gt;constructDsn($params, $dsnOptions),\r\n                $params&#x5B;'user'] ?? '',\r\n                $params&#x5B;'password'] ?? '',\r\n                $driverOptions\r\n            );\r\n        } catch (\\PDOException $exception) {\r\n            throw PDOException::new($exception);\r\n        }\r\n\r\n        return new Connection(new PDOConnection($pdo));\r\n    }\r\n\r\n    \/**\r\n     * Constructs the Sqlsrv PDO DSN.\r\n     *\r\n     * @param mixed&#x5B;]  $params\r\n     * @param string&#x5B;] $connectionOptions\r\n     *\r\n     * @throws Exception\r\n     *\/\r\n    private function constructDsn(array $params, array $connectionOptions): string\r\n    {\r\n        $dsn = 'odbc:Driver=FreeTDS;TDS_Version=7.4';\r\n\r\n        if (isset($params&#x5B;'host'])) {\r\n            $dsn .= sprintf(';Server=%s', $params&#x5B;'host']);\r\n        } elseif (isset($params&#x5B;'port'])) {\r\n            throw PortWithoutHost::new();\r\n        }\r\n        if (isset($params&#x5B;'port'])) {\r\n            $dsn .= sprintf(';Port=%s', $params&#x5B;'port']);\r\n        }\r\n\r\n        if (isset($params&#x5B;'dbname'])) {\r\n            $connectionOptions&#x5B;'Database'] = $params&#x5B;'dbname'];\r\n        }\r\n\r\n        if (isset($params&#x5B;'MultipleActiveResultSets'])) {\r\n            $connectionOptions&#x5B;'MultipleActiveResultSets'] = $params&#x5B;'MultipleActiveResultSets'] ? 'true' : 'false';\r\n        }\r\n\r\n        return $dsn.$this-&gt;getConnectionOptionsDsn($connectionOptions);\r\n    }\r\n\r\n    \/**\r\n     * Converts a connection options array to the DSN.\r\n     *\r\n     * @param string&#x5B;] $connectionOptions\r\n     *\/\r\n    private function getConnectionOptionsDsn(array $connectionOptions): string\r\n    {\r\n        $connectionOptionsDsn = '';\r\n\r\n        foreach ($connectionOptions as $paramName =&gt; $paramValue) {\r\n            $connectionOptionsDsn .= sprintf(';%s=%s', $paramName, $paramValue);\r\n        }\r\n\r\n        return $connectionOptionsDsn;\r\n    }\r\n}\r\n<\/pre>\n<p>Alles andere bringt bereits die <code>sqlsrv<\/code> Implementierung von Doctrine mit.<\/p>\n<p>Die neue Klasse wird nun wie folgt in der <code>doctrine.yaml<\/code> eingebunden:<\/p>\n<pre class=\"brush: yaml; title: ; notranslate\" title=\"\">\r\ndoctrine:\r\n    dbal:\r\n        # IMPORTANT: this configuration requires FreeTDS installed!\r\n        driver_class: App\\Lib\\OdbcMssql\\Driver\r\n        driver: pdo_mssql\r\n        dbname:           &quot;%env(resolve:MSSQL_DB)%&quot;\r\n        user:             &quot;%env(resolve:MSSQL_USER)%&quot;\r\n        password:         &quot;%env(resolve:MSSQL_PASS)%&quot;\r\n        host:             &quot;%env(resolve:MSSQL_HOST)%&quot;\r\n        port: 1433\r\n        options: \r\n          'client_charset': 'UTF-8'\r\n        server_version: '2008'\r\n<\/pre>\n<p>So wird der eigene Driver f\u00fcr die Connection verwendet, der <code>pdo_mssql<\/code> aber f\u00fcr s\u00e4mtliche anderen Operationen, beispielsweise das Erzeugen von SQL oder die Hydration der Datentypen. Das Feld server_version muss auf 2008 stehen, damit <code>pdo_mssql<\/code> weiss, welche Syntaxvariante verwendet wird. 2008 ist hier der kleinste gemeinsame Nenner. In der <code>.env<\/code> oder <code>.env.local<\/code> m\u00fcssen noch die 4 Variablen mit den Datenbankeinstellungen erstellt werden.<\/p>\n<p>Danach kann die Verbindung einmal getestet werden. Dies geht am einfachsten \u00fcber die Konsole.<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\r\n$ php bin\/console  dbal:run-sql --connection infor &quot;SELECT blah FROM blubb WHERE 0=1&quot;\r\n\r\nIn ExceptionConverter.php line 39:\r\n                                                                                                                                                                         \r\n  An exception occurred while executing a query: SQLSTATE&#x5B;42S02]: Base table or view not found: 208 &#x5B;FreeTDS]&#x5B;SQL Server]Invalid object name 'blubb'. (SQLExecute&#x5B;208]   \r\n  at .\/ext\/pdo_odbc\/odbc_stmt.c:259)                                                                                                                                     \r\n<\/pre>\n<p>Die Fehlermeldung bedeutet, dass die Verbindung zum Datenbankserver erfolgreich war und die gew\u00e4hlte Datenbank existiert. Da wie einen Phantasienamen abgefragt haben, ist ein &#8222;Invalid object name&#8220; genau das, was wir erwarten. Eigenlich sind<\/p>\n<h2>Wie gehts weiter?<\/h2>\n<p>Eigentlich kann man nun ganz normal mit Doctrine arbeiten. Wenn man jedoch mit einer bestehenden Datenbank arbeiten m\u00f6chte, muss man h\u00e4ndisch die Entities erzeugen, da bei MS-SQL die Erzeugung aus der Datenbank eher m\u00e4\u00dfig funktioniert.<\/p>\n<p>In einem zuk\u00fcnftigen Tutorial werde ich einmal beschreiben, wie man gleichzeitig auf eine MS-SQL und eine MariaDB Datenbank zugreifen kann und dabei immer den korrekten Entity-Manager mit der passenden Datenbankverbindung bekommt.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-338","post","type-post","status-publish","format-standard","hentry","category-allgemein"],"_links":{"self":[{"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/posts\/338","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/comments?post=338"}],"version-history":[{"count":10,"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/posts\/338\/revisions"}],"predecessor-version":[{"id":349,"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/posts\/338\/revisions\/349"}],"wp:attachment":[{"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/media?parent=338"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/categories?post=338"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/carsten.familie-schumann.info\/blog\/wp-json\/wp\/v2\/tags?post=338"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}