I am trying to integrate the WHAnonymous API in my symfony project.
I have included it in my project using composer install and it is now in my vendor folder.
But I am not understanding how to import it into my project!
This is my manager class.
<?php
namespace AppBundle\Managers;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class WhatsAppManager
{
private $test;
/**
* Constructor
*/
public function __construct()
{
$this->test =1;
}
public function sendMessage()
{
$username = ""; // Your number with country code, ie: 34123456789
$nickname = ""; // Your nickname, it will appear in push notifications
$debug = true; // Shows debug log
// Create a instance of WhastPort.
$w = new WhatsProt($username, $nickname, $debug);
var_dump("In send message method");
}
}
?>
I have used
require_once 'whatsprot.class.php';
and
require_once 'Whatsapp\Bundle\Chat-api\src\whatsprot.class.php';
and
use Whatsapp\Bundle\Chat-api\Whatsprot
But it is just not working.
Please tell me the right way to do it!
And is there something i should do when i am using 3rd party vendors in symfony.
I did look into the documentation of the WHanonymous but i found only snippets of code to use it and not the way to import it.
Git repo for WHAnonymous : https://github.com/WHAnonymous
The class doesn't have a namespace, but is correctly loaded by the autoload system created my composer. So you can reference to the class without any include or require directive but simply with a \ as example:
// Create a instance of WhastPort.
$w = new \WhatsProt($username, $nickname, $debug);
Hope this help
Related
I'm stuck on an error that I can't solve, I need help please.
Attempted to load class "MockStorageStrategy" from namespace "MangoPay\Tests\Mocks".
Did you forget a "use" statement for another namespace?
my code:
<?php
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use App\Entity\User;
use MangoPay;
use MangoPay\Tests\Mocks\MockStorageStrategy;
class CallApiService
{
private $mangoPayApi;
private $client;
public function __construct(HttpClientInterface $httpClient)
{
$this->client = $httpClient;
$this->mangoPayApi = new MangoPay\MangoPayApi();
$this->mangoPayApi->Config->ClientId = $_ENV['CLIENT_ID'];
$this->mangoPayApi->Config->ClientPassword = $_ENV['API_KEY'];
// $this->mangoPayApi->Config->TemporaryFolder = '/some/path/';
$this->mangoPayApi->OAuthTokenManager->RegisterCustomStorageStrategy(new MockStorageStrategy());
//$this->mangoPayApi->Config->BaseUrl = 'https://api.sandbox.mangopay.com';
}
public function createProfilMango($form)
{
$userMango = $this->client->request(
'POST',
'https://api.sandbox.mangopay.com/v2.01/' . '%env(CLIENT_ID)%' . '/users/natural',
[
$UserNatural = new MangoPay\UserNatural(),
$UserNatural->FirstName = $form['firstname']->getData(),
$UserNatural->LastName = $form['lastname']->getData(),
$UserNatural->Email = $form['email']->getData(),
$UserNatural->Address = new \MangoPay\Address(),
$UserNatural->Address->AddressLine1 = $form['streetNumber']->getData() . $form['address']->getData(),
$UserNatural->Address->AddressLine2 = "",
$UserNatural->Address->City = $form['city']->getData(),
$UserNatural->Address->Region = "",
$UserNatural->Address->PostalCode = $form['zipCode']->getData(),
$UserNatural->Address->Country = "FR",
$UserNatural->Birthday = $form['birthday']->getData(),
$UserNatural->Nationality = $form['nationality']->getData(),
$UserNatural->CountryOfResidence = "FR",
$Result = $this->mangoPayApi->Users->Create($UserNatural),
]
);
return $userMango;
}
}
The namespace has been checked and it is correct, concerning the dependencies the http-client and mangopay/php-sdk-v2 have been installed.
Using classes from a Test namespace are sometimes not added to the autoloader - as you should not use them in your application. A look at that package's composer.json shows this: the namespace MangoPay is routed to the folder MangoPay (see autoload for this), while the class you want to use is stored in another folder and loaded only through autoload-dev. This section is not evaluated in case you are solely using this package in your own application.
If you really want to use that class MockStorageStrategy (which is only provided for the package's internal test suite, not for being used by the application!), you should copy it to your own application namespace.
I filled a bug but it seams I'm off :p
I just want to replace the service Symfony\Component\Translation\Reader\TranslationReader (translation.reader) with my own class. In fact I want to know how to replace any service of SF4 if I want
translation.reader::addLoader() is normally called by the framework but if I decorate with my own class addLoader is not called.
Can you tell me how I can just drop replace my own service ?
https://github.com/symfony/symfony/issues/28843
Symfony version(s) affected: 4.1.6
Description
Cannot decorate translation.reader (I want to change the default i18n file loading process)
How to reproduce
copy/adapt Symfony\Component\Translation\Reader\TranslationReader to App\Translation\Reader\TranslationReader
Follow https://symfony.com/doc/current/service_container/service_decoration.html
Modify services.yaml
Symfony\Component\Translation\Reader\TranslationReader: ~
App\Translation\Reader\TranslationReader:
decorates: Symfony\Component\Translation\Reader\TranslationReader
#translation.reader: '#App\Translation\Reader\TranslationReader'
Without the alias : the new service is ignored
With the alias : read() is trigger but not addLoader()
Here are the generated injection file getTranslationReaderService.php :
<?php
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the private 'App\Translation\Reader\TranslationReader' shared autowired service.
include_once $this->targetDirs[3].'/vendor/symfony/translation/Reader/TranslationReaderInterface.php';
include_once $this->targetDirs[3].'/src/Translation/Reader/TranslationReader.php';
return $this->privates['App\Translation\Reader\TranslationReader'] = new \App\Translation\Reader\TranslationReader();
By default it looks like :
use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
// This file has been auto-generated by the Symfony Dependency Injection Component for internal use.
// Returns the private 'translation.reader' shared service.
include_once $this->targetDirs[3].'/vendor/symfony/translation/Reader/TranslationReaderInterface.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Reader/TranslationReader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/LoaderInterface.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/ArrayLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/FileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/PhpFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/YamlFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/XliffFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/PoFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/MoFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/QtFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/CsvFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/IcuResFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/IcuDatFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/IniFileLoader.php';
include_once $this->targetDirs[3].'/vendor/symfony/translation/Loader/JsonFileLoader.php';
$this->privates['translation.reader'] = $instance = new \Symfony\Component\Translation\Reader\TranslationReader();
$a = ($this->privates['translation.loader.yml'] ?? $this->privates['translation.loader.yml'] = new \Symfony\Component\Translation\Loader\YamlFileLoader());
$b = ($this->privates['translation.loader.xliff'] ?? $this->privates['translation.loader.xliff'] = new \Symfony\Component\Translation\Loader\XliffFileLoader());
$instance->addLoader('php', ($this->privates['translation.loader.php'] ?? $this->privates['translation.loader.php'] = new \Symfony\Component\Translation\Loader\PhpFileLoader()));
$instance->addLoader('yaml', $a);
$instance->addLoader('yml', $a);
$instance->addLoader('xlf', $b);
$instance->addLoader('xliff', $b);
$instance->addLoader('po', ($this->privates['translation.loader.po'] ?? $this->privates['translation.loader.po'] = new \Symfony\Component\Translation\Loader\PoFileLoader()));
$instance->addLoader('mo', ($this->privates['translation.loader.mo'] ?? $this->privates['translation.loader.mo'] = new \Symfony\Component\Translation\Loader\MoFileLoader()));
$instance->addLoader('ts', ($this->privates['translation.loader.qt'] ?? $this->privates['translation.loader.qt'] = new \Symfony\Component\Translation\Loader\QtFileLoader()));
$instance->addLoader('csv', ($this->privates['translation.loader.csv'] ?? $this->privates['translation.loader.csv'] = new \Symfony\Component\Translation\Loader\CsvFileLoader()));
$instance->addLoader('res', ($this->privates['translation.loader.res'] ?? $this->privates['translation.loader.res'] = new \Symfony\Component\Translation\Loader\IcuResFileLoader()));
$instance->addLoader('dat', ($this->privates['translation.loader.dat'] ?? $this->privates['translation.loader.dat'] = new \Symfony\Component\Translation\Loader\IcuDatFileLoader()));
$instance->addLoader('ini', ($this->privates['translation.loader.ini'] ?? $this->privates['translation.loader.ini'] = new \Symfony\Component\Translation\Loader\IniFileLoader()));
$instance->addLoader('json', ($this->privates['translation.loader.json'] ?? $this->privates['translation.loader.json'] = new \Symfony\Component\Translation\Loader\JsonFileLoader()));
return $instance;
You can see that loaders are not injected when I do the decorating...
I'm not sure exactly if this is the root of your problem, but here are some remarks. Hopefully this will help you find a solution, even though I'm not actually given a full answer to your question.
1) Some translation services in Symfony are called only during the cache warmup phase. Whenever you change your config, or do a bin/console cache:clear, you'll see these classes are run, and they generate translations in your var/cache/<env>/translations/ folder.
2) You can try to make sure that in your cache, the classe loaded by var/cache/<env>/Container<...>/getTranslation_ReaderService.php is yours and not the default one like this:
$this->privates['translation.reader'] =
new \Symfony\Component\Translation\Reader\TranslationReader();
3) I also encountered a similar issue in the dev environment, where I was trying to replace Symfony\Component\Translation\Translator with my own service, and didn't manage to get my methods to be called at first. Part of the explanation was that when the Symfony Profiler is enabled, Symfony does something like this (in src<env>DebugProjectContainer.php>):
$this->services['translator'] = new \Symfony\Component\Translation\DataCollectorTranslator(
($this->privates['translator.default'] ?? $this->getTranslator_DefaultService())
);
and the DataCollectorTranslator itself is a wrapper for whichever translator it gets as its constructor argument.
I know this is not a perfect answer but hopefully this will help you find your way to a solution.
I've managed to make it work... but please feel free to comment
I had to create a TranslatorPass to add loaders to the decorating service injection file.
<?php
namespace App\Translation\DependencyInjection;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use App\Translation\Reader\TranslationReader;
class TranslatorPass implements CompilerPassInterface
{
private $readerServiceId;
private $loaderTag;
public function __construct(string $readerServiceId = TranslationReader::class, string $loaderTag = 'translation.loader')
{
$this->readerServiceId = $readerServiceId;
$this->loaderTag = $loaderTag;
}
public function process(ContainerBuilder $container)
{
$loaders = array();
$loaderRefs = array();
foreach ($container->findTaggedServiceIds($this->loaderTag, true) as $id => $attributes) {
$loaderRefs[$id] = new Reference($id);
$loaders[$id][] = $attributes[0]['alias'];
if (isset($attributes[0]['legacy-alias'])) {
$loaders[$id][] = $attributes[0]['legacy-alias'];
}
}
if ($container->hasDefinition($this->readerServiceId)) {
$definition = $container->getDefinition($this->readerServiceId);
foreach ($loaders as $id => $formats) {
foreach ($formats as $format) {
$definition->addMethodCall('addLoader', array($format, $loaderRefs[$id]));
}
}
}
}
}
I've put it in the Kernel.php
protected function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new TranslatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 1000);
}
then
bin/console cache:clear
et voilĂ !
I am integrating quickbooks with my laravel app. After integration I got this error,
PHP Warning: require_once(../QuickBooks.php): failed to open stream:
No such file or directory in
/home/vipin/projects/development/Quickbook/config/app.php on line 2
PHP Fatal error: require_once(): Failed opening required '../QuickBooks.php'
(include_path='.:/usr/share/php:/home/ubuntu/projects/development/Quickbook/vendor/consolibyte/quickbooks')
in /home/ubuntu/projects/development/Quickbook/config/app.php on line
2
Here is my controller Quickbook.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
// require_once '../QuickBooks.php';
use App\Http\Requests;
class QuickBooksController extends Controller
{
private $IntuitAnywhere;
private $context;
private $realm;
public function __construct(){
if (!\QuickBooks_Utilities::initialized(env('QBO_DSN'))) {
// Initialize creates the neccessary database schema for queueing up requests and logging
\QuickBooks_Utilities::initialize(env('QBO_DSN'));
}
$this->IntuitAnywhere = new \QuickBooks_IPP_IntuitAnywhere(env('QBO_DSN'), env('QBO_ENCRYPTION_KEY'), env('QBO_OAUTH_CONSUMER_KEY'), env('QBO_CONSUMER_SECRET'), env('QBO_OAUTH_URL'), env('QBO_SUCCESS_URL'));
}
public function qboConnect(){
if ($this->IntuitAnywhere->check(env('QBO_USERNAME'), env('QBO_TENANT')) && $this->IntuitAnywhere->test(env('QBO_USERNAME'), env('QBO_TENANT'))) {
// Set up the IPP instance
$IPP = new \QuickBooks_IPP(env('QBO_DSN'));
// Get our OAuth credentials from the database
$creds = $this->IntuitAnywhere->load(env('QBO_USERNAME'), env('QBO_TENANT'));
// Tell the framework to load some data from the OAuth store
$IPP->authMode(
\QuickBooks_IPP::AUTHMODE_OAUTH,
env('QBO_USERNAME'),
$creds);
if (env('QBO_SANDBOX')) {
// Turn on sandbox mode/URLs
$IPP->sandbox(true);
}
// This is our current realm
$this->realm = $creds['qb_realm'];
// Load the OAuth information from the database
$this->context = $IPP->context();
return true;
} else {
return false;
}
}
public function qboOauth(){
if ($this->IntuitAnywhere->handle(env('QBO_USERNAME'), env('QBO_TENANT')))
{
; // The user has been connected, and will be redirected to QBO_SUCCESS_URL automatically.
}
else
{
// If this happens, something went wrong with the OAuth handshake
die('Oh no, something bad happened: ' . $this->IntuitAnywhere->errorNumber() . ': ' . $this->IntuitAnywhere->errorMessage());
}
}
public function qboSuccess(){
return view('qbo_success');
}
public function qboDisconnect(){
$this->IntuitAnywhere->disconnect(env('QBO_USERNAME'), env('QBO_TENANT'),true);
return redirect()->intended("/yourpath");// afer disconnect redirect where you want
}
public function createCustomer(){
$CustomerService = new \QuickBooks_IPP_Service_Customer();
$Customer = new \QuickBooks_IPP_Object_Customer();
$Customer->setTitle('Ms');
$Customer->setGivenName('Shannon');
$Customer->setMiddleName('B');
$Customer->setFamilyName('Palmer');
$Customer->setDisplayName('Shannon B Palmer ' . mt_rand(0, 1000));
// Terms (e.g. Net 30, etc.)
$Customer->setSalesTermRef(4);
// Phone #
$PrimaryPhone = new \QuickBooks_IPP_Object_PrimaryPhone();
$PrimaryPhone->setFreeFormNumber('860-532-0089');
$Customer->setPrimaryPhone($PrimaryPhone);
// Mobile #
$Mobile = new \QuickBooks_IPP_Object_Mobile();
$Mobile->setFreeFormNumber('860-532-0089');
$Customer->setMobile($Mobile);
// Fax #
$Fax = new \QuickBooks_IPP_Object_Fax();
$Fax->setFreeFormNumber('860-532-0089');
$Customer->setFax($Fax);
// Bill address
$BillAddr = new \QuickBooks_IPP_Object_BillAddr();
$BillAddr->setLine1('72 E Blue Grass Road');
$BillAddr->setLine2('Suite D');
$BillAddr->setCity('Mt Pleasant');
$BillAddr->setCountrySubDivisionCode('MI');
$BillAddr->setPostalCode('48858');
$Customer->setBillAddr($BillAddr);
// Email
$PrimaryEmailAddr = new \QuickBooks_IPP_Object_PrimaryEmailAddr();
$PrimaryEmailAddr->setAddress('support#consolibyte.com');
$Customer->setPrimaryEmailAddr($PrimaryEmailAddr);
if ($resp = $CustomerService->add($this->context, $this->realm, $Customer))
{
//print('Our new customer ID is: [' . $resp . '] (name "' . $Customer->getDisplayName() . '")');
//return $resp;
//echo $resp;exit;
//$resp = str_replace('{','',$resp);
//$resp = str_replace('}','',$resp);
//$resp = abs($resp);
return $this->getId($resp);
}
else
{
//echo 'Not Added qbo';
print($CustomerService->lastError($this->context));
}
}
public function addItem(){
$ItemService = new \QuickBooks_IPP_Service_Item();
$Item = new \QuickBooks_IPP_Object_Item();
$Item->setName('My Item');
$Item->setType('Inventory');
$Item->setIncomeAccountRef('53');
if ($resp = $ItemService->add($this->context, $this->realm, $Item))
{
return $this->getId($resp);
}
else
{
print($ItemService->lastError($this->context));
}
}
public function addInvoice($invoiceArray,$itemArray,$customerRef){
$InvoiceService = new \QuickBooks_IPP_Service_Invoice();
$Invoice = new \QuickBooks_IPP_Object_Invoice();
$Invoice = new QuickBooks_IPP_Object_Invoice();
$Invoice->setDocNumber('WEB' . mt_rand(0, 10000));
$Invoice->setTxnDate('2013-10-11');
$Line = new QuickBooks_IPP_Object_Line();
$Line->setDetailType('SalesItemLineDetail');
$Line->setAmount(12.95 * 2);
$Line->setDescription('Test description goes here.');
$SalesItemLineDetail = new QuickBooks_IPP_Object_SalesItemLineDetail();
$SalesItemLineDetail->setItemRef('8');
$SalesItemLineDetail->setUnitPrice(12.95);
$SalesItemLineDetail->setQty(2);
$Line->addSalesItemLineDetail($SalesItemLineDetail);
$Invoice->addLine($Line);
$Invoice->setCustomerRef('67');
if ($resp = $InvoiceService->add($this->context, $this->realm, $Invoice))
{
return $this->getId($resp);
}
else
{
print($InvoiceService->lastError());
}
}
public function getId($resp){
$resp = str_replace('{','',$resp);
$resp = str_replace('}','',$resp);
$resp = abs($resp);
return $resp;
}
}
Config/app.php
<?php
require_once '../QuickBooks.php';
return [
'qbo_token' => env('QUICKBOOK_TOKEN'),
'qbo_consumer_key' => env('QBO_OAUTH_CONSUMER_KEY'),
'qbo_consumer_secret' => env('QBO_CONSUMER_SECRET'),
'qbo_sandbox' => env('QBO_SANDBOX'),
'qbo_encryption_key' => env('QBO_ENCRYPTION_KEY'),
'qbo_username' => env('QBO_USERNAME'),
'qbo_tenant' => env('QBO_TENANT'),
'qbo_auth_url' => 'http://app.localhost:8000/qbo/oauth',
'qbo_success_url' => 'http://app.localhost:8000/qbo/success',
'qbo_mysql_connection' => 'mysqli://'. env('DB_USERNAME') .':'. env('DB_PASSWORD') .'#'. env('DB_HOST') .'/'. env('DB_DATABASE'),
There are several areas to improve on here with the given code & approach.
As Anton correctly points out, you should not be directly requiring any of the quickbooks library files. If you've loaded this in via Composer then they will be automatically loaded because the Composer autoloader will load the QuickBooks file from the vendor. This is correct for Laravel as well as general Composer-based applications - the only difference with Laravel is that there isn't a specific Laravel Package ServiceProvider that's been written for this SDK, but that doesn't matter.
The QuickBooks library tries to jump on top of autoloading any class that starts with 'QuickBooks', so you're better off making a QuickBooks folder for your controller class. This is more of a 'gotcha' and has been pointed out in the repo issues.
The reason you're getting the Driver/.php error is because you have not specified your QBO_DSN, or have done so incorrectly - this DSN environment variable that you're passing to the initialisation is being run through parse_url() in the SDK code, coming up false or null and breaking the auto-loader for initalisation. If this was set to a proper connection string (e.g. mysqli://username:password#host:port/database and note that port must be a number or it's considered malformed), it would correctly process the DSN and continue to load the page. Be aware that initialisation will attempt to parse and fetch the network address of the host, so you can't just put a dummy value in there and expect it to work - this needs to exist first.
You're mixing your environment variables and application configuration, without using either of them properly. If you wanted your DB connection string (a.k.a. QBO_DSN) to be constructed a particular way into the application configuration setting qbo_mysql_connection, then you should be using the configuration setting when trying to initialise/load/etc. Instead of using env('QBO_DSN'), you should be using config('app.qbo_mysql_connection') to load the constructed version from your app settings. Typically you would not be loading so many environment variables into a controller at all - that should be handled by the application, and then the controller calling the application configuration so it's agnostic of how they were defined.
You shouldn't need to require anything from inside the app configuration file either - that file is just for configuration variables being set up.
Since the QuickBooks SDK isn't properly namespaced (yet), there isn't a nice PSR-4 way of loading (and use-ing) the classes, but it's still good practice to use use clauses at the top of the file (e.g. use QuickBooks_Utilities;) so that you can use the classes without fear of forgetting the preceding backslash (i.e. no more \QuickBooks_Utilities, just QuickBooks_Utilities in usage) - there are several instances in the given code where this has been forgotten, and will not work because the Laravel application is namespaced and will look for those classes in the App\Http\Controllers namespace (e.g. errors like "Cannot find class App\Http\Controllers\QuickBooks_Utilities").
Indentation - pick a style (e.g. tabs, 2-space, PSR-2, etc) and then stick to it. Run phpcs or some other clean-up tool over all of your code before committing to your repository or posting on SO - readability is important!
Using require instead of autoloader is a bad practice in modern frameworks (and generally in modern PHP). I highly recommend using the package manager (eg composer) to properly add modules to the project.
For example, to add a quickbooks library into the project using composer, you need to run only one command:
composer require consolibyte/quickbooks
Add this line in footer of Config/app.php
require_once '../QuickBooks.php';
I have an API written using Symfony2 that I'm trying to write post hoc tests for. One of the endpoints uses an email service to send a password reset email to the user. I'd like to mock out this service so that I can check that the right information is sent to the service, and also prevent an email from actually being sent.
Here's the route I'm trying to test:
/**
* #Route("/me/password/resets")
* #Method({"POST"})
*/
public function requestResetAction(Request $request)
{
$userRepository = $this->get('app.repository.user_repository');
$userPasswordResetRepository = $this->get('app.repository.user_password_reset_repository');
$emailService = $this->get('app.service.email_service');
$authenticationLimitsService = $this->get('app.service.authentication_limits_service');
$now = new \DateTime();
$requestParams = $this->getRequestParams($request);
if (empty($requestParams->username)) {
throw new BadRequestHttpException("username parameter is missing");
}
$user = $userRepository->findOneByUsername($requestParams->username);
if ($user) {
if ($authenticationLimitsService->isUserBanned($user, $now)) {
throw new BadRequestHttpException("User temporarily banned because of repeated authentication failures");
}
$userPasswordResetRepository->deleteAllForUser($user);
$reset = $userPasswordResetRepository->createForUser($user);
$userPasswordResetRepository->saveUserPasswordReset($reset);
$authenticationLimitsService->logUserAction($user, UserAuthenticationLog::ACTION_PASSWORD_RESET, $now);
$emailService->sendPasswordResetEmail($user, $reset);
}
// We return 201 Created for every request so that we don't accidently
// leak the existence of usernames
return $this->jsonResponse("Created", $code=201);
}
I then have an ApiTestCase class that extends the Symfony WebTestCase to provide helper methods. This class contains a setup method that tries to mock the email service:
class ApiTestCase extends WebTestCase {
public function setup() {
$this->client = static::createClient(array(
'environment' => 'test'
));
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$this->mockEmailService = $mockEmailService;
}
And then in my actual test cases I'm trying to do something like this:
class CreatePasswordResetTest extends ApiTestCase {
public function testSendsEmail() {
$this->mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
}
So now the trick is to get the controller to use the mocked version of the email service. I have read about several different ways to achieve this, so far I've not had much luck.
Method 1: Use container->set()
See How to mock Symfony 2 service in a functional test?
In the setup() method tell the container what it should return when it's asked for the email service:
static::$kernel->getContainer()->set('app.service.email_service', $this->mockEmailService);
# or
$this->client->getContainer()->set('app.service.email_service', $this->mockEmailService);
This does not effect the controller at all. It still calls the original service. Some write ups I've seen mention that the mocked service is 'reset' after a single call. I'm not even seeing my first call mocked out so I'm not certain this issue is affecting me yet.
Is there another container I should be calling set on?
Or am I mocking out the service too late?
Method 2: AppTestKernel
See: http://blog.lyrixx.info/2013/04/12/symfony2-how-to-mock-services-during-functional-tests.html
See: Symfony2 phpunit functional test custom user authentication fails after redirect (session related)
This one pulls me out of my depth when it comes to PHP and Symfony2 stuff (I'm not really a PHP dev).
The goal seems to be to change some kind of foundation class of the website to allow my mock service to be injected very early in the request.
I have a new AppTestKernel:
<?php
// app/AppTestKernel.php
require_once __DIR__.'/AppKernel.php';
class AppTestKernel extends AppKernel
{
private $kernelModifier = null;
public function boot()
{
parent::boot();
if ($kernelModifier = $this->kernelModifier) {
$kernelModifier($this);
$this->kernelModifier = null;
};
}
public function setKernelModifier(\Closure $kernelModifier)
{
$this->kernelModifier = $kernelModifier;
// We force the kernel to shutdown to be sure the next request will boot it
$this->shutdown();
}
}
And a new method in my ApiTestCase:
// https://stackoverflow.com/a/19705215
protected static function getKernelClass(){
$dir = isset($_SERVER['KERNEL_DIR']) ? $_SERVER['KERNEL_DIR'] : static::getPhpUnitXmlDir();
$finder = new Finder();
$finder->name('*TestKernel.php')->depth(0)->in($dir);
$results = iterator_to_array($finder);
if (!count($results)) {
throw new \RuntimeException('Either set KERNEL_DIR in your phpunit.xml according to http://symfony.com/doc/current/book/testing.html#your-first-functional-test or override the WebTestCase::createKernel() method.');
}
$file = current($results);
$class = $file->getBasename('.php');
require_once $file;
return $class;
}
Then I alter my setup() to use the kernel modifier:
public function setup() {
...
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
static::$kernel->setKernelModifier(function($kernel) use ($mockEmailService) {
$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
});
$this->mockEmailService = $mockEmailService;
}
This works! However I now can't access the container in my other tests when I'm trying to do something like this:
$c = $this->client->getKernel()->getContainer();
$repo = $c->get('app.repository.user_password_reset_repository');
$resets = $repo->findByUser($user);
The getContainer() method returns null.
Should I be using the container differently?
Do I need to inject the container into the new kernel? It extends the original kernel so I don't really know why/how it's any different when it comes to the container stuff.
Method 3: Replace the service in config_test.yml
See: Symfony/PHPUnit mock services
This method requires that I write a new service class that overrides the email service. Writing a fixed mock class like this seems less useful than a regular dynamic mock. How can I test that certain methods have been called with certain parameters?
Method 4: Setup everything inside the test
Going on #Matteo's suggestion I wrote a test that did this:
public function testSendsEmail() {
$mockEmailService = $this->getMockBuilder(EmailService::class)
->disableOriginalConstructor()
->getMock();
$mockEmailService->expects($this->once())
->method('sendPasswordResetEmail');
static::$kernel->getContainer()->set('app.service.email_service', $mockEmailService);
$this->client->getContainer()->set('app.service.email_service', $mockEmailService);
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
}
This test fails because the expected method sendPasswordResetEmail wasn't called:
There was 1 failure:
1) Tests\Integration\Api\MePassword\CreatePasswordResetTest::testSendsEmail
Expectation failed for method name is equal to <string:sendPasswordResetEmail> when invoked 1 time(s).
Method was expected to be called 1 times, actually called 0 times.
Thanks to Cered's advice I've managed to get something working that can test that the emails I expect to be sent actually are. I haven't been able to actually get the mocking to work so I'm a bit reluctant to mark this as "the" answer.
Here's a test that checks that an email is sent:
public function testSendsEmail() {
$this->client->enableProfiler();
$this->post(
"/me/password/resets",
array(),
array("username" => $this->user->getUsername())
);
$mailCollector = $this->client->getProfile()->getCollector('swiftmailer');
$this->assertEquals(1, $mailCollector->getMessageCount());
$collectedMessages = $mailCollector->getMessages();
$message = $collectedMessages[0];
$this->assertInstanceOf('Swift_Message', $message);
$this->assertEquals('Reset your password', $message->getSubject());
$this->assertEquals('info#example.com', key($message->getFrom()));
$this->assertEquals($this->user->getEmail(), key($message->getTo()));
$this->assertContains(
'This link is valid for 24 hours only.',
$message->getBody()
);
$resets = $this->getResets($this->user);
$this->assertContains(
$resets[0]->getToken(),
$message->getBody()
);
}
It works by enabling the Symfony profiler and inspecting the swiftmailer service. It's documented here: http://symfony.com/doc/current/email/testing.html
How can I run a test "within PHP" instead of using the 'phpunit' command? Example:
<?php
require_once 'PHPUnit/Extensions/SeleniumTestCase.php';
class MySeleniumTest extends PHPUnit_Extensions_SeleniumTestCase {
protected function setUp() {
$this->setBrowser("*firefox");
$this->setBrowserUrl("http://example.com/");
}
public function testMyTestCase() {
$this->open("/");
$this->click("//a[#href='/contact/']");
}
}
$test = new MySeleniumTest();
//I want to run the test and get information about the results so I can store them in the database, send an email etc.
?>
Or do I have to write the test to a file, invoke phpunit via system()/exec() and parse the output? :(
Just use the Driver that's included.
require_once 'PHPUnit/Extensions/SeleniumTestCase/Driver.php';
//You may need to load a few other libraries. Try it.
Then you need to set it up like SeleniumTestCase does:
$driver = new PHPUnit_Extensions_SeleniumTestCase_Driver;
$driver->setName($browser['name']);
$driver->setBrowser($browser['browser']);
$driver->setHost($browser['host']);
$driver->setPort($browser['port']);
$driver->setTimeout($browser['timeout']);
$driver->setHttpTimeout($browser['httpTimeout']);
Then just:
$driver->open('/');
$driver->click("//a[#href='/contact/']");
Here's an example from the phpunit docs:
<?php
require_once 'PHPUnit/Framework.php';
require_once 'ArrayTest.php';
require_once 'SimpleTestListener.php';
// Create a test suite that contains the tests
// from the ArrayTest class.
$suite = new PHPUnit_Framework_TestSuite('ArrayTest');
// Create a test result and attach a SimpleTestListener
// object as an observer to it.
$result = new PHPUnit_Framework_TestResult;
$result->addListener(new SimpleTestListener);
// Run the tests.
$suite->run($result);
?>
The code for SimpleTestListener is on the same page.