Trying to setup fixtures for my Phalcon tests, but it fails.
Can you figure out what went wrong with my implementation ?
Usage
phalcon 7.2
Docker version 18.09.0-ce-beta1
docker-compose version 1.22.0
phpunit6.5.8
dbunit 3.0.3
Current state
Connecting to the database by using the "getConnection" function is successful
create dataSet(YAML type)
create getDataSet function, but fails to set up fixture and cleanup
unitTestCase.php
<?php
use Phalcon\Di;
use Phalcon\Mvc\Model\Manager as ModelsManager;
use Phalcon\Test\UnitTestCase as PhalconTestCase;
use PHPUnit\DbUnit\TestCaseTrait;
require_once "CustomHelper.php";
// config of phpunit
abstract class UnitTestCase extends PhalconTestCase
{
use TestCaseTrait;
/**
* #var bool
*/
private $_loaded = false;
public function setUp()
{
parent::setUp();
$di = Di::getDefault();
$di->set(
"modelsManager",
function () {
return new ModelsManager();
}
);
$di["modelsMetadata"] = function () {
$metadata = new \Phalcon\Mvc\Model\Metadata\Files(
[
"metaDataDir" => joinPaths(__DIR__, "/path/to/metadata/"),
]
);
return $metadata;
};
$di->setShared('mockControllerHelper', new mockControllerHelper());
$di->setShared('helper', new Helpers());
$di->setShared('config', function () {
return require CONFIG_DIR . 'app.php';
});
$di->setShared('db', function () {
$config = $this->getConfig();
// database configuration from app.php
$class = 'Phalcon\Db\Adapter\Pdo\\' . $config->database->adapter;
$params = [
'host' => $config->database->host,
'username' => $config->database->username,
'password' => $config->database->password,
'dbname' => $config->database->dbname,
];
if ($config->database->adapter == 'Postgresql') {
unset($params['charset']);
}
$connection = new $class($params);
return $connection;
});
$this->setDi($di);
$this->_loaded = true;
}
public function tearDown()
{
/* This static call cleans up the Mockery container used by the current test, and run any verification tasks needed for our expectations. */
\Mockery::close();
}
/**
* Check if the test case is setup properly
*
* #throws \PHPUnit_Framework_IncompleteTestError;
*/
public function __destruct()
{
if (!$this->_loaded) {
throw new \PHPUnit_Framework_IncompleteTestError(
"Please run parent::setUp()."
);
}
}
// Setting part of dbunit
static private $pdo = null;
private $conn = null;
final public function getConnection()
{
require CONFIG_DIR . 'app.php';
if ($this->conn === null) {
if (self::$pdo == null) {
self::$pdo = new PDO(
'mysql:host='. $globalConfig->database->host .';dbname=test',
$globalConfig->database->username,
$globalConfig->database->password
);
}
$this->conn = $this->createDefaultDBConnection(self::$pdo, 'dbname=test');
}
return $this->conn;
}
}
TestControllerTest.php
<?php
namespace Test\Controllers;
use PHPUnit\DbUnit\DataSet\YamlDataSet;
class DatabaseTest extends \UnitTestCase
{
protected function getDataSet()
{
return new YamlDataSet(__DIR__ . "/../DataSet/dataSet.yml");
}
public function testsTableRow() {
$this->assertEquals(1, $this->getConnection()->getRowCount('testRow'), "");
}
}
DataSet
testRow:
-
id: 1
name: 'test'
text: 'hello, world'
Response
Failed asserting that 0 matches expected 1.
Note
When I use a real mysql database connection in getConnection, and call $this->getConnection()->getRowCount('testRow'), the result is correct.
Therefore, the problem seems to come from loading the yaml dataset
supplementation1
The contents of $this->getConnection is :
(execute print_r($this->getConnection(), false);)
(
[connection:protected] => PDO Object
(
)
[metaData:protected] => PHPUnit\DbUnit\Database\Metadata\MySQL Object
(
[schemaObjectQuoteChar:protected] => `
[pdo:protected] => PDO Object
(
)
[schema:protected] => dbname=test
[truncateCommand:protected] => TRUNCATE
)
)
The contents of $this->getDataSet() is :
(execute print_r($this->getDataSet(), false);)
(
[tables:protected] => Array
(
[testRow] => PHPUnit\DbUnit\DataSet\DefaultTable Object
(
[tableMetaData:protected] => PHPUnit\DbUnit\DataSet\DefaultTableMetadata Object
(
[columns:protected] => Array
(
[0] => id
[1] => name
[2] => text
)
[primaryKeys:protected] => Array
(
)
[tableName:protected] => testRow
)
[data:protected] => Array
(
[0] => Array
(
[id] => 1
[name] => test
[text] => hello, world
)
)
[other:PHPUnit\DbUnit\DataSet\AbstractTable:private] =>
)
)
[parser:protected] => PHPUnit\DbUnit\DataSet\SymfonyYamlParser Object
(
)
)
Dbunit has to perform some setup operations and they are defined in PHPUnit\DbUnit\TestCaseTrait in setUp() method (you can see more code details here. When you inherit PHPUnit\DbUnit\TestCase you mostly do not have to worry about it (other than just making sure to call parent::setUp() from your testcase). When using functionality directly from the trait you have to make sure that dbunit's set up is also invoked.
You can achieve this by smth like below:
<?php
use Phalcon\Test\UnitTestCase as PhalconTestCase;
use PHPUnit\DbUnit\TestCaseTrait;
abstract class UnitTestCase extends PhalconTestCase
{
use TestCaseTrait {
setUp as protected dbunitSetUp;
};
public function setUp()
{
parent::setUp(); // phalcon setup
$this->dbuintSetUp(); // dbunit setup
}
}
For the sample above only the lines relevant to the problem are included, skipping other stuff like getConnection, etc.
Related
I am using API Platform 2.6
As I need to export some contents to PDF, I had to create a custom encoder for that to work.
1st, I registered a new format, so I can request application/pdf content.
#File: config/packages/api_platform:
api_platform:
....
formats:
....
pdf: ['application/pdf']
2nd, Created a PdfEncoder that extends EncoderInterface
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
public const FORMAT = 'pdf';
public function __construct(
private readonly Environment $twig,
private readonly Pdf $pdf,
) {
}
public function encode($data, $format, array $context = []): string
{
$content = $this->twig->render('pdf/template.html.twig', ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
public function supportsEncoding($format): bool
{
return self::FORMAT === $format;
}
}
3d, on the resource, I created the appropriate call:
#File src/Resource/MyResource.php
#[ApiResource(
itemOperations: [
'export' => [
'method' => 'GET',
'path' => '/myResource/{id}/export',
'requirements' => ['id' => '\d+'],
'output' => MyResourceView::class
],
],
)]
class MyResource
{
public int $id;
/** Other Attributes **/
}
As you can see, On the PdfEncoder class, I hard coded the path to the Twig Template,
But As I need to export other resources to PDF, and they are using different templates, I need to pass this template path as an option to the encoder, Maybe on the context variable would be great.
Here is what I am looking for.
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
....
public function encode($data, $format, array $context = []): string
{
$template = $context['export']['template']?? null;
if (!$template){
throw new \Exception('Twig template is not defined');
}
$content = $this->twig->render($template, ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
...
}
Is there a way to accomplish this?
I tried adding that on the Resource, but ApiPlatform deleted them before having them on the context array
#File src/Resource/MyResource.php
#[ApiResource(
itemOperations: [
'export' => [
'method' => 'GET',
'path' => '/myResource/{id}/export',
'requirements' => ['id' => '\d+'],
'output' => MyResourceView::class,
'export' => [
'template' => 'pdf/template.html.twig',
]
],
],
)]
class MyResource
{
public int $id;
/** Other Attributes **/
}
This is what I've come up with so far,
This helps me to get the template name dynamically.
#File: src/Encodre/PdfEncoder.php:
class PdfEncoder implements EncoderInterface
{
public const FORMAT = 'pdf';
public function __construct(
private readonly Environment $twig,
private readonly Pdf $pdf,
) {
}
public function encode($data, $format, array $context = []): string
{
$template = sprintf('pdf/%s.html.twig', lcfirst($context['output']['name']));
if (!$this->twig->getLoader()->exists($template)) {
throw new \RuntimeException(sprintf('Missing template for %s', $context['output']['class']));
}
$content = $this->twig->render($template, ['data' => $data]);
$filename = sprintf('/tmp/%s.pdf', uniqid());
$this->pdf->generateFromHtml($content, $filename);
return file_get_contents($filename);
}
public function supportsEncoding($format): bool
{
return self::FORMAT === $format;
}
}
$context['output']['class'] is the name of the View class (base name)
So this way, for every View, I have a twig file.
I won't mark this answer as the solution as I think there might be nicer/recommended way to do that.
I try to create a custom form control for editing a EntityCollection in my Entity
My customFormType is like :
$hiddenInput = $builder->create($builder->getName(), TextType::class);
$hiddenInput->addModelTransformer(new CallbackTransformer(
function ($idsString) use ($repository) {
dump($idsString);
// $ids = array_filter(explode(',', $idsString));
// $a = array_map(function($id) use ($repository) {
// if ($id !== null && strlen($id) > 0) {
// return $repository->find($id);
// }
// }, $ids);
// return $a;
return [];
},
function ($ids) use ($repository) {
$collection = new ArrayCollection();
foreach ($ids as $id) {
$collection->add($repository->find($id));
}
return $collection;
}
));
$hiddenInput->addViewTransformer(new CallbackTransformer(
function ($idsArray) {
if ($idsArray === null) {
return '';
}
return implode(',', $idsArray);
},
function ($idsString) {
return explode(',', $idsString);
}
));
$builder->add($hiddenInput);
My first issue is in my ModelTransformer, $idsString still get null value ... I didn't find why ...
My second issue is if I submit the form and dump my entity I see this:
I didn't understand why new value for the collection is added in a new key ...
I didn't understand how it's possible ...
( my adder can't take an array, my setter ... why merge old value with the news it makes no sense ... so my entity seem right, and only the form has issue :/ )
Is there way to filter collection, e.g.
class Company{
/**
* #ORM\OneToMany(targetEntity="App\Entity\User", mappedBy="company", cascade={"persist","remove"})
*/
public $users;
}
on way to check that company have users. But i need to filter on company side, so send request /api/companies?somefilter.
So point is there way to check is collection empty?
You can add a boolean column in companies where you set to true when create a user relation.
So you can add a BooleanFilter to you company entity for check companies which have users.
/**
* #ApiResource
* #ApiFilter(BooleanFilter::class, properties={"hasUsers"})
*/
Or you can create a CustomFilter where you input true o false and get companies with users throught queryBuilder
https://api-platform.com/docs/core/filters/#creating-custom-doctrine-orm-filters
<?php
// api/src/Filter/RegexpFilter.php
namespace App\Filter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\AbstractContextAwareFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use Doctrine\ORM\QueryBuilder;
final class RegexpFilter extends AbstractContextAwareFilter
{
protected function filterProperty(string $property, $value, QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $operationName = null)
{
// otherwise filter is applied to order and page as well
if (
!$this->isPropertyEnabled($property, $resourceClass) ||
!$this->isPropertyMapped($property, $resourceClass)
) {
return;
}
$parameterName = $queryNameGenerator->generateParameterName($property);
// Generate a unique parameter name to avoid collisions with other filters
$queryBuilder
->andWhere(sprintf('REGEXP(o.%s, :%s) = 1', $property, $parameterName))
->setParameter($parameterName, $value);
}
// This function is only used to hook in documentation generators (supported by Swagger and Hydra)
public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}
$description = [];
foreach ($this->properties as $property => $strategy) {
$description["regexp_$property"] = [
'property' => $property,
'type' => 'string',
'required' => false,
'swagger' => [
'description' => 'Filter using a regex. This will appear in the Swagger documentation!',
'name' => 'Custom name to use in the Swagger documentation',
'type' => 'Will appear below the name in the Swagger documentation',
],
];
}
return $description;
}
}
I'm trying to create a test in PHPSpec for an function that calls a function on another object with differing parameters. So far, my attempts have caused several different errors so I'll outline what I have so far.
The most recent error:
- it should find all realm data
method call:
- fetch(LeagueOfData\Adapters\Request\RealmRequest:000000001212f67d000000001262e5c6 Object (
'apiDefaults' => Array &0 (
'region' => 'euw'
)
'format' => null
'data' => null
'query' => null
'where' => Array &0
))
on Double\AdapterInterface\P51 was not expected, expected calls were:
fetch(exact(Double\RequestInterface\P50:000000001212f607000000001262e5c6 Object (
'objectProphecy' => Prophecy\Prophecy\ObjectProphecy Object (*Prophecy*)
The PHPSpec file:
class JsonRealmsSpec extends ObjectBehavior
{
function let(AdapterInterface $adapter, LoggerInterface $logger, RequestInterface $request)
{
// fetch called with multiple request objects but we're not interested in the exact data it returns yet.
$adapter->fetch($request)->willReturn(['test data']);
$this->beConstructedWith($adapter, $logger);
}
function it_should_find_all_realm_data()
{
$this->findAll()->shouldReturnArrayOfRealms();
}
function getMatchers()
{
return [
'returnArrayOfRealms' => function($realms) {
foreach ($realms as $realms) {
if (!$realm instanceof Realm) {
return false;
}
}
return true;
}
];
}
}
And the actual function being tested:
class JsonRealms implements RealmService
{
const REGIONS = ['euw', 'eune', 'na'];
private $source;
private $log;
private $realms;
public function __construct(AdapterInterface $adapter, LoggerInterface $log)
{
$this->source = $adapter;
$this->log = $log;
}
public function findAll() : array
{
$this->realms = [];
foreach (self::REGIONS as $region) {
$request = new RealmRequest(['region' => $region]);
$response = $this->source->fetch($request);
$this->realms[] = new Realm($realm['cdn'], $realm['v'], $region);
}
return $this->realms;
}
}
I'm sure I'm probably missing something really obvious but for the life of me I cannot see it right now.
So it turns out I was missing something obvious, I was trying to solve it with a single mock call, as opposed to one for each case:
function let(AdapterInterface $adapter, LoggerInterface $logger) {
$request = new RealmRequest(['region' => 'euw']);
$adapter->fetch($request)->willReturn([
'cdn' => 'http://test.url/euw',
'v' => '7.4.3'
]);
$request = new RealmRequest(['region' => 'eune']);
$adapter->fetch($request)->willReturn([
'cdn' => 'http://test.url/eune',
'v' => '7.4.3'
]);
$request = new RealmRequest(['region' => 'na']);
$adapter->fetch($request)->willReturn([
'cdn' => 'http://test.url/na',
'v' => '7.4.3'
]);
}
This appropriately sets up the mock adapter so that we can test the service is correctly creating the Realm objects.
I wrote a custom views handler, that mark message private to 0 or 1 or 2; any is value of hers label [MARK_READ,ARK_NEW,...] :
function mydevel_views_data() {
$data['mydevel']['table']['group'] = t('mydevel');
$data['mydevel']['table']['join'] = array(
// Exist in all views.
'#global' => array(),
);
$data['mydevel']['mydevel_isnewmessage'] = array(
'title' => t('is new message field'),
'help' => t('is new message field.'),
'field' => array(
'handler' => 'mydevel_handler_field_isnewmessage',
'click sortable' => TRUE,
),
'filter' => array(
'handler' => 'mydevel_handler_filter_isnewmessage',
),
);
and wrote a filed handler that work properly; message_mark function is wrote on mydevel module file and work currectly; if the message is new that filed label row by "now":
class mydevel_handler_field_isnewmessage extends views_handler_field_numeric {
var $field_alias = 'mydevel_field_isnewmessage';
function query() {
}
function option_definition() {
$options = parent::option_definition();
//dsm($this);
return $options;
}
function options_form(&$form, &$form_state) {
parent::options_form($form, $form_state);
}
function get_value($values, $field = NULL) {
return intval(message_mark($values->mid, $values->message_timestamp));
}
function render($values) {
$value = $this->get_value($values);
$value_theme = theme('mark', array('type' => $value));
return $value_theme;
}
}
Now, i want to write a views filter handler that filter on that filed on numeric mode [0 or 1 or 2] or on check list mode [all, read, new, updated]. but I don't want to overwite query function on filter handler and want to use from value that returned by this common handler filed (mydevel_handler_filter_isnewmessage) that added to views filed. can wrote this idea by extend the standard views handler? what i do my dears? I wrote this but not work: this is return error
class mydevel_handler_filter_isnewmessage extends views_handler_filter_numeric {
var $always_multiple = TRUE;
function option_definition() {
$options = parent::option_definition();
return $options;
}
function operators() {
$operators = parent::operators();
return $operators;
}
function query() {
}
}
tank you a lot.