Unser CRM in der Company ist selbst geschrieben – auf einer etwas spannenden Code-Basis, welche man als Einsteiger in das Projekt nicht unbedingt sofort versteht. Vieles wird global geregelt, Klassen sind eher dekorativ und wenig flexibel und eine Template-Engine oder Design-Patterns sucht man vergeblich. Es gibt kein Framework wie Zend oder Symfony drumherum – wirklich jede Zeile stammt aus eigener Hand.
Heute würde man das natürlich nicht mehr so machen – aber es ist, wie man so schön sagt, „historisch gewachsen“ und würde einen enormen Aufwand mit sich ziehen, den Code aufzuräumen oder gar komplett neu zu schreiben.
Ich wollte jedenfalls eine API für Alfred 2 schreiben, welche nach außen sicher ist und mir Kundendaten, SSH-Zugänge und allen nützlichen Kram rausgibt. Sodass ich am Ende das CRM gar nicht mehr öffnen muss. Hat im ersten Anlauf auch geklappt, sah nur nicht besonders schön aus. Wenigstens bei der API wollte ich ein richtiges Framework aufsetzen und das Ganze „2016-Style“ aufbauen. Mir schien es wie ein geeigneter Zeitpunkt, endlich mal etwas mit dem Slim-Framwork umzusetzen.
Also los – als erstes also die Quellen mal geklont und losgelegt.
composer create-project slim/slim-skeleton [my-app-name]
Die index.php habe ich etwas verkürzt und ein paar includes direkt dort mit aufgenommen. Mir gefällt es nicht ständig zwischen Dateien hin und her zu springen.
if (PHP_SAPI == 'cli-server') { // To help the built-in PHP dev server, check if the request was actually for // something which should probably be served as a static file $file = __DIR__ . $_SERVER['REQUEST_URI']; if (is_file($file)) { return false; } } require __DIR__ . '/vendor/autoload.php'; use Slim\Http\Request as Request; use Slim\Http\Response as Response; session_start(); // Instantiate the app $app = new \Slim\App([ 'settings' => [ 'displayErrorDetails' => true, // Monolog settings 'logger' => [ 'name' => 'slim-app', 'path' => __DIR__ . '/logs/app.log', ], ], ]); // Set up dependencies $container = $app->getContainer(); // monolog $container['logger'] = function ($c) { $settings = $c->get('settings')['logger']; $logger = new Monolog\Logger($settings['name']); $logger->pushProcessor(new Monolog\Processor\UidProcessor()); $logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], Monolog\Logger::WARNING)); return $logger; }; // Register routes require_once(__DIR__ . '/src/alfred.php'); // Run app $app->run();
Wie man sieht, habe ich das Logging mit Monolog übernommen, welches auch schon im Skeleton so vorgesehen ist. Dafür sind andere Dinge (wie das PHP-Templating) rausgeflogen. Da ich keine .phtml-Dateien parsen und ausliefern möchte, sondern nur JSON oder XML, wäre mir das nur im Wege.
Soweit, so gut. Die Anforderung an meine REST-Api waren allerdings noch Basic-Authentication mit der bestehenden Nutzer-Basis. Da das alles Objektorientiert ist, habe ich einfach den Autoloader vom CRM hinzugefügt, und konnte ab dann alle Klassen verwenden. Dafür habe ich eine weitere Composer-Dependency hinzugefügt, welche es per Middleware erlaubt, eine Authentication durchzuführen. Dazu wird einfach eine Callback-Funktion aufgerufen, welche den ganzen Kram erledigt.
Die Rede ist von „Slim Basic Auth“ von Mika Tuupola. Über composer ist auch diese Abhängigkeit schnell installiert:
composer require tuupola/slim-basic-auth
Nun könnte man sich natürlich ein Array aufbauen, welches alle erlaubten User und deren Passwörter enthält, und dieses an die Middleware übergeben. Aber eine eigene Klasse und eine Callback-Funktion zu nutzen ist doch wesentlich interessanter:
// Register middleware class Auth { /** * @var \myNamespace\User|null */ private $user = null; public function getUser() { return $this->user; } public function login($user, $password) { if ($user && $password) { $userObj = \myNameSpace\User::loadByUsername($user); $this->user = $userObj; return $user->validatePassword($password); } return false; } } $auth = new Auth(); $app->add(new \Slim\Middleware\HttpBasicAuthentication([ 'secure' => false, // Für live-Betrieb true! 'authenticator' => function ($arguments) use ($auth) { $user = $arguments["user"]; $password = $arguments["password"]; return $auth->login($user, $password); }, "error" => function (Request $request, Response $response, $arguments) { $data = array( 'status' => 'error', 'message' => $arguments['message'] ); return $response->write(json_encode($data, JSON_UNESCAPED_SLASHES)); } ]));
Wie man sieht macht die eigene Auth-Klasse nicht viel besonderes. Sie hält einfach nur den eingeloggten Benutzer für uns bereit, damit wir diesen in den späteren Requests einfach verwenden können. Falls ihr dafür eine coolere Lösung habt, immer her damit! Ich fand es so auf den ersten Blick recht praktisch. Immerhin können wir die Referenz zu dem Auth-Objekt nun immer per use-Keyword mit in die anonymen Funktionen reichen. Also sehr verträglich mit der bestehenden Anwendung und auch einfach nachträglich zu integrieren, falls schon alles steht.
Achso: Scheitert die Authentication, bricht der Request ab. Das heißt, man kann sich sicher sein, dass die login-Funktion true zurückgegeben hat, wenn man sich in der Verarbeitung eines Requests befindet. Außer, man hat die Authentication für bestimmte Routen ausgestellt – das geht nämlich auch. Am besten dazu einmal die Doku lesen.
Nun zu den eigentlichen Requests. Diese sind in Slim total schnell erklärt. Im folgenden ein Beispiel,
$app->get('/alfred/call/[{number}]', function (Slim\Http\Request $request, Slim\Http\Response $response, $args) use ($app, $auth) { /** @var $this Slim\Container */ if ($number = $args['number']) { $user = $auth->getUser(); $response->getBody()->write($user->call($number)); } else { $response->getBody()->write(0); } return $response; });
Ziemlich einfach, oder? Es wird eine Route definiert, welche einen Platzhalter für einen Parameter enthält. Dieser Wert wird in einem assoziativen Array im Parameter „args“ übergeben an eine anonyme Funktion übergeben. Über das use-Keyword komme ich auch hier sehr einfach an mein Auth-Objekt, welches meinen eingeloggten User kapselt. Diesen hole ich mir dann einfach und führe eine Funktion aus, welche das CRM schon vorgibt. In diesem Beispiel wird ein neuer Anruf für den Mitarbeiter über Asterisk eingeleitet.
Das praktische dabei ist, dass man die Logik immer direkt neben dem Pfad stehen hat. Es muss also keine neue Klasse erstellt werden, welche dann wiederum von überall die Infos sammelt. Alles an einer Stellt und extrem gut wartbar.
Natürlich kann die API nun sehr viel mehr – aber ich denke, dass Prinzip ist an dieser Stelle klar geworden.
Wichtig wäre noch zu sagen, dass das Reponse-Objekt immutable ist. Bedeutet, dass man keine Attribute verändern kann, nur getter existieren und somit beim Aufruf von beispielsweise der folgenden Code-Zeile immer eine neue Instanz zurückgegeben wird.
$response->withAddedHeader('Content-Type', 'application/json');
Wichtig ist, dass man immer eine Instanz des Reponse-Objektes zurück gibt. Dieses wird dann vom Framework ausgewertet und kann so an den Client übermittelt werden. Ziemliches cooles Konzept!
Jedenfalls hatte ich die API nach sehr kurzer Zeit auf Slim umgeschrieben und bin insgesamt von dem Framework sehr begeistert! Gerade in solchen Projekten, wo man kein größeres Framework zur Verfügung hat und alles selbst programmiert wurde, eignet sich Slim für eine kleine API.
Ich hoffe jedenfalls, dass ich bald wieder ein Projekt mit Slim umsetzen darf!