Symfony 4: Keep SQLite PDO connection in test + controller + twig extensions - php

My situation
I have a Symfony 4.2 project with the following structure:
src
Controller
Service
Twig Extensions
Templates
Tests
I use a database class, which internally creates a PDO connection. If i run my tests with PHPUnit, my database class has to switch from mysql to sqlite. Everything works fine here.
My problem
I can not keep the once created Database instance, when running just one test. Symfony seems to recreate it during the test: when inside a Twig template which uses a Twig extension. Because the database class uses
new \PDO('sqlite::memory:');
it loses created tables and therefore the test fails. I am aware of, that the Database instance (with PDO reference) gets reseted after each test, but in my situation i only have one test. How can i ensure, that it re-uses the Database instance?
Here the related code
InstanceExtension class provides the function title, which is used in a Twig template and has to access the database.
<?php
namespace App\TwigExtension;
use App\Service\Database;
use Twig\TwigFunction;
use Twig\Extension\AbstractExtension;
class InstanceExtension extends AbstractExtension
{
protected $db;
/**
* #param Database $db
*/
public function __construct(Database $db)
{
$this->db = $db;
}
/**
* Tries to return the title/name for a given ID.
*
* #param string $subject
* #param string|array $tables
* #param string $lang Default is 'de'
*
* #return string label or id, if no title/name was found
*/
public function title(string $subject, $tables, string $lang = 'de'): string
{
return $this->db->get_instance_title($subject, $tables, $lang);
}
}
In my services.yaml the Database class is set to public (which should enable reusing it, doesn't it?):
App\Service\Database:
public: true
Database class
Here is the part of the Database class which initializes the PDO connection. Production code, which uses MySQL instead, removed:
<?php
declare(strict_types=1);
namespace App\Service;
class Database
{
/**
* #param Config $app_config
*
* #throws \Exception
*/
public function __construct(Config $app_config)
{
global $sqlite_pdo;
try {
// non test
// ..
// test environment
} else {
$this->db_engine = 'sqlite';
// workaround to ensure we have the same PDO instance everytime we use the Database instance
// if we dont use it, in Twig extensions a new Database instance is created with a new SQLite
// database, which is empty.
if (null == $sqlite_pdo) {
$pdo = new \PDO('sqlite::memory:');
$sqlite_pdo = $pdo;
} else {
$pdo = $sqlite_pdo;
}
}
} catch (\PDOException $e) {
if (\strpos((string) $e->getMessage(), 'could not find driver') !== false) {
throw new \Exception(
'Could not create a PDO connection. Is the driver installed/enabled?'
);
}
if (\strpos((string) $e->getMessage(), 'unknown database') !== false) {
throw new \Exception(
'Could not create a PDO connection. Check that your database exists.'
);
}
// Don't leak credentials directly if we can.
throw new \Exception(
'Could not create a PDO connection. Please check your username and password.'
);
}
if ('mysql' == $this->db_engine) {
// ...
}
// default fetch mode is associative
$pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
// everything odd will be handled as exception
$pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
$this->db = $pdo;
}
// ...
}
One test looks kinda like:
<?php
class SalesChannelControllerTest extends TestCase
{
public function setUp()
{
parent::setUp();
// init Database class, using SQLite
$this->init_db();
// further setup function calls
// SalesChannelController is a child of
// Symfony\Bundle\FrameworkBundle\Controller\AbstractController
$this->fixture = new SalesChannelController(
$this->db
// ...
);
}
/**
* Returns a ready-to-use instance of the database. Default adapter is SQLite.
*
* #return Database
*/
protected function init_db(): Database
{
// config parameter just contains DB credentials
$this->db = new Database($this->config);
}
public function test_introduction_action()
{
// preparations
/*
* run action
*
* which renders a Twig template, creates a Response and returns it.
*/
$response = $this->fixture->introduction_action($this->request, $this->session, 'sales_channel1');
/*
* check response
*/
$this->assertEquals(200, $response->getStatusCode());
}
}
My current workaround is to store the PDO instance in a global variable and re-use it, if required.
<?php
global $sqlite_pdo;
// ...
// inside Database class, when initializing the PDO connection
if (null == $sqlite_pdo) {
$pdo = new \PDO('sqlite::memory:');
$sqlite_pdo = $pdo;
} else {
$pdo = $sqlite_pdo;
}
If you need more info, please tell me. Thanks for your time and help in advance!

Related

How do I fetch the PHP DI container?

How do I load a database container using PHP DI?
This is one of the variations I have tried up until now.
Settings.php
<?php
use MyApp\Core\Database;
use MyApp\Models\SystemUser;
return [
'Database' => new Database(),
'SystemUser' => new SystemUser()
];
init.php
$containerBuilder = new \DI\ContainerBuilder();
$containerBuilder->addDefinitions('Settings.php');
$container = $containerBuilder->build();
SystemUserDetails.php
<?php
namespace MyApp\Models\SystemUser;
use MyApp\Core\Database;
use MyApp\Core\Config;
use MyApp\Helpers\Session;
/**
*
* System User Details Class
*
*/
class SystemUserDetails
{
/*=================================
= Variables =
=================================*/
private $db;
/*===============================
= Methods =
================================*/
/**
*
* Construct
*
*/
public function __construct(Database $db)
{
# Get database instance
// $this->db = Database::getInstance();
$this->db = $db;
}
/**
Too few arguments to function MyApp\Models\SystemUser\SystemUserDetails::__construct(), 0 passed in /www/myapp/models/SystemUser.php on line 54 and exactly 1 expected
File: /www/myapp/models/SystemUser/SystemUserDetails.php
Shouldn't the database get loaded automatically?
Trace:
Currrently, My main index.php file extends init.php which is the file where it create the container (pasted code part in the post).
Then I call the App class, which fetches the URL(mysite.com/login/user_login) and instantiate a new controller class and run the mentioned method, in this case, it's the first page - MyApp/Contollers/Login.php.
The user_login method fetches the credentials, validate them, and if they are valid, uses the SystemUser object to login.
SystemUser class:
namespace MyApp\Models;
class SystemUser
{
public $id;
# #obj SystemUser profile information (fullname, email, last_login, profile picture, etc')
protected $systemUserDetatils;
public function __construct($systemUserId = NULL)
{
# Create systemUserDedatils obj
$this->systemUserDetatils = new \MyApp\Models\SystemUser\SystemUserDetails();
# If system_user passed
if ( $systemUserId ) {
# Set system user ID
$this->id = $systemUserId;
# Get SysUser data
$this->systemUserDetatils->get($this);
} else {
# Check for sysUser id in the session:
$systemUserId = $this->systemUserDetatils->getUserFromSession();
# Get user data from session
if ( $systemUserId ) {
# Set system user ID
$this->id = $systemUserId;
# Get SysUser data
$this->systemUserDetatils->get($this);
}
}
}
}
PHP-DI is working correctly.
In your SystemUser class you are doing:
$this->systemUserDetatils = new \MyApp\Models\SystemUser\SystemUserDetails();
The constructor for SystemUserDetails requires a Database object, which you are not passing.
By calling new directly, you are not using PHP-DI. By doing this you are hiding the dependency, which is exactly what you are supposedly trying to avoid if you want to use a dependency injection system.
If SystemUser depends ("needs") SystemUserDetails, the dependency should be explicit (e.g. declared in its constructor).
Furthermore: You do not need a definitions file for a system like this. And the definitions file you show in your question doesn't follow the best practices recommended by PHP-DI.
Your design is far from perfect, and I'm not sure of your end-goals, but if you did something like this, it could work:
<?php
// src/Database.php
class Database {
public function getDb() : string {
return 'veryDb';
}
}
<?php
// src/SystemUserDetails.php
class SystemUserDetails {
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function getDetails() {
return "Got details using " . $this->db->getDb() . '.';
}
}
<?php
// src/SystemUser.php
class SystemUser {
protected $details;
public function __construct(SystemUserDetails $details, $userId=null) {
$this->details = $details;
}
public function getUser() {
return "Found User. " .$this->details->getDetails();
}
}
<?php
//init.php
require_once('vendor/autoload.php');
// build the container. notice I do not use a definition file.
$containerBuilder = new \DI\ContainerBuilder();
$container = $containerBuilder->build();
// get SystemUser instance from the container.
$userInstance = $container->get('SystemUser');
echo $userInstance->getUser(), "\n";
Which results in:
Found User. Got details using veryDb.

PHPunit Global Variable returns NULL

I'm new to php Unit test, but I could installed it and make it work (PHP 7.1.2 / phpUnit 6.1.1 on a Mac / MAMP system). But I now I can't let it test my app. Probably due to a strange class architectural hierarchy.
In my autoload.php I load my classes like that:
/* get timing functions */
$th = new TimingHelper();
/* Get basic functions */
$basic = new basic();
/* Get App data (param: fake localhost true / false) */
$App = new App();
/* Start DB Connection */
$db = new db(0);
/* Get usr data (param: fake user mode true / false, user id) */
$usr = new User();
There are even more classes... ;)
Now inside the User Class I get the previous classes like that:
class User {
/* get DB connection */
private $db, $App, $tr, $th, $basic;
public function __construct() {
/* get DB connection */
Global $db, $App, $tr, $th, $basic;
$this->db = $db;
$this->App = $App;
$this->tr = $tr;
$this->th = $th;
$this->basic = $basic;
/* Start timing */
$this->th->start();
}
}
That's probably not way one should do it, but it works ;)
But now if I want to use phpUnit, I heard it ignores global objects, so
var_dump($this->th);
is NULL.
Is there no way, that I can use phpUnit with my class system? If not, what do I need to change?
I tried to extend each class, but then my app was SUPER slow...

pass parameter from pipeline middleware to factory class in zend expressive 2

I want to connect to different database according to URL. I try to set request attribute and get that attribute in *Factory.php.
I edit autoload/pipeline.php:
<?php
$app->pipe(UrlHelperMiddleware::class);
$app->pipe(\App\Action\Choose::class);
$app->pipeDispatchMiddleware();
in Choose.php I implement process() like this:
<?php
public function process(ServerRequestInterface $request, DelegateInterface $delegate)
{
/** #var RouteResult $route */
$route = $request->getAttribute(RouteResult::class);
if ($route->getMatchedRouteName() == 'FirstRoute' or $route->getMatchedRouteName() == 'SecondRoute') {
$request = $request->withAttribute('DB_NAME', $route->getMatchedParams()['param']);
}
return $delegate->process($request);
}
The main problem is in *Factory.php I don't access to request.
Any try to access to Interop\Http\ServerMiddleware\MiddlewareInterface or Psr\Http\Message\ServerRequestInterface in *Factory.php raises same error.
Is there any way pass parameter from pipeline middleware to factory class?
If you use zend-servicemanager you can try this (just a theory, not tested):
Create 2 database factories in your config:
'db.connection.a' => DbFactoryA::class,
'db.connection.b' => DbFactoryB::class,
Then depending on the route, in Choose you load the connection you need and pass it to the container as the default connection.
$db = $container->get('db.connection.a');
$container->setService('db.connection.default', $db);
And now in all following middleware you can grab the default connection.
UPDATE:
As mentioned in the comments, this requires the container to be injected which is considered bad practice. How about you wrap the two in a common connection class and set the required from Choose:
class Connection
{
private $connection;
/**
* #var ConnectionInterface
*/
private $db_a;
/**
* #var ConnectionInterface
*/
private $db_b;
public function __construct(ConnectionInterface $a, ConnectionInterface $b)
{
$this->db_a = $a;
$this->db_b = $b;
}
public function setConnection($connection)
{
if ($connection === 'a') {
$this->connection = $this->db_a;
return;
}
$this->connection = $this->db_b;
}
public function getConnection()
{
return $this->connection;
}
}
Or you can inject only the config and create the database connection that's really needed. Store it in a property for caching (like the container does).

PHPUnit RabbitMQ: write test for create connection function

I'm facing the following problem. I've wrote a function that create a connection object (AMQPConnection) given the required parameters. Now I want to write the corresponding unit test. I just don't know how to do it without having the RabbitMQ broker running. Here is the function in question:
public function getConnection($hostKey, array $params)
{
$connection = null;
try {
$connection = new AMQPConnection(
$params['host'],
$params['port'],
$params['username'],
$params['password'],
$params['vhost']
);
// set this server as default for next connection connectionAttempt
$this->setDefaultHostConfig($hostKey, $params);
return $connection;
} catch (\Exception $ex) {
if ($this->isAttemptExceeded()) {
return $connection;
} else {
// increment connection connectionAttempt
$this->setConnectionAttempt($this->getConnectionAttempt() + 1);
return $this->getConnection($hostKey, $params);
}
}
}
You usually don't test code like this as a Unittest since the result would more likely tell you that your server is installed correctly and not that your code is working.
Do you test if PDO returns a valid database connection?
It could make sense if you test your installation but not to test if the php c libs are working correctly.
You should add an ability to change the class being instantiated.
Add an ability to change the connector class via constructor or setter, with a default AMQPConnector class by default. Use it to create a connector object. Example
Create a mocked connector class for unit tests. Example
Use it setting the mock class via constructor or setter in tests. Example
You could also do assertions on what arguments are passed to the connector constructor.
Another way would be to use amqp interop so you are not coupled to any implementation. Much easier to tests as you deal with pure interfaces only.
#chozilla
#zerkms
So thanks to your hints, I've decided to use Closure in order to isolate the code portion connecting to the server.
Here is the closure:
$connectionFunction = function ($params) {
return new AMQPStreamConnection(
$params['host'],
$params['port'],
$params['username'],
$params['password'],
$params['vhost']
);
};
And here the modified getConnection() function
/**
* #param string $hostKey The array key of the host connection parameter set
* #param array $params The connection parameters set
* #return null|AMQPStreamConnection
*/
public function getConnection($hostKey, array $params)
{
$connection = null;
try {
$connection = call_user_func($connectionFunction, $params);
// set this server as default for next connection connectionAttempt
$this->setDefaultHostConfig($hostKey, $params);
return $connection;
} catch (\Exception $ex) {
if ($this->isAttemptExceeded()) {
return $connection;
} else {
// increment connection connectionAttempt
$this->setConnectionAttempt($this->getConnectionAttempt() + 1);
return $this->getConnection($hostKey, $params);
}
}
}
For the unit test I did the following:
$mockConnection = $this->getMockBuilder('PhpAmqpLib\Connection\AMQPStreamConnection')
->disableOriginalConstructor()
->getMock();
$connectionFunction = function ($params) use ($mockConnection) {
return $mockConnection;
};
Or this to have an Exception.
$connectionFunction = function ($params) {
throw new \Exception;
};
N.B.: I'm using AMQPStreamConnection in the getConnection() function since AMQPConnection is marked as deprecated in PhpAmqpLib

phpunit test a mapper with silex php

I would like test my class LocationMapper whose the aim is make CRUD operations:
<?php
class LocationMapper {
/*
* Database connection.
*/
var $db = NULL;
/*
* Constructor function. Loads model from database if the id is known.
*
* #param $db
* Database connection
*/
function __construct($app) {
$this->db = $app['db'];
}
/**
* Load data from the db for an existing user.
*/
function read($app, $id) {
$statement = $this->db->prepare('SELECT * FROM location WHERE id = :id');
$statement->execute(array(':id' => $id));
$data = $statement->fetch(PDO::FETCH_ASSOC);
$comments = $app['CommentMapper']->loadAllByLocation($id);
$location = new Location($data['latitude'], $data['longitude'], $data['address'], $data['name'], $data['checked'], $comments);
$location->set('id', $data['id']);
return $location;
}
...
}
And in my index.php, I create my database connection and my mapper like this :
require_once __DIR__ . '/../config.php';
// Add the database connection as a shared service to the container.
$db_string = 'mysql:host=' . $app['db.hostname'] .';dbname=' . $app['db.dbname'];
$app['db'] = $app->share(function ($app) use ($db_string) {
try {
return new PDO($db_string, $app['db.username'], $app['db.password']);
}
catch (PDOException $e) {
$app->abort(500, 'Unable to connect to database. Check your configuration');
}
});
// Initialize mappers
$app['LocationMapper'] = $app->share(function () use ($app) {
return new LocationMapper($app);
});
How test the class LocationMapper with a real database ?
I tested this :
class LocationMapperTest extends PHPUnit_Framework_TestCase
{
protected $locationMapper;
protected $app;
public function setUp() {
$app = new Silex\Application();
// Add the database connection as a shared service to the container.
$db_string = 'mysql:host=' . $app['db.hostname'] .';dbname=' . $app['db.dbname'];
$app['db'] = $app->share(function ($app) use ($db_string) {
try {
return new PDO($db_string, $app['db.username'], $app['db.password']);
}
catch (PDOException $e) {
$app->abort(500, 'Unable to connect to database. Check your configuration');
}
});
$locationMapper = new LocationMapper();
}
public function createTest() {
$this->assertEquals(0, 0);
}
}
but I have an error PHP Fatal error: Class 'Silex\Application' not found
I guess you've installed Silex using Composer, so you are including the Composer autoload at the very beginning of your application (probably inside your config.php) to automatically include all needed files.
The problem is that PHPUnit is not using your config file, so it's not autoloading the classes: PHPUnit won't use an autoload file unless you told it to do so.
To tell PHPUnit to load your autoload file you have two options:
The first one is to use the bootstrap switch while executing the tests on the command line. That way, PHPUnit will include the bootstrap file at the beginning of the tests execution. If you pass it your autoload file, it will autoload your classes.
The second options is to use a configuration file, where you specify your bootstrap file.
Apart from that, a few observations about your code. Your unit tests shouldn't use your real database for testing. That way, your tests will modify the real database, making the tests unrepeteable (you'd have to re-populate the database with data if you delete rows, for example), slow, and, more importantly, you may affect real data.
I'd recommend you to mock your database, or use a database for testing purposes at least.

Categories