I have configured slim to write logs to log files as the standard way. But this is not effective when we want to search large and all the logs at a given time. So I want to write those logs to a separate sqlite DB.
My question is how can I set the log writer to write the messages (as done in the Zend framework) ?
P S: I know that I can create a PDO object and use the queries. But I don't want to change the existing code. Just prefer to set the writer and let the framework do the job for me.
I managed to do this as follows,
Create the sqlite connection
$sqlite = new PDO('sqlite:./logs/log.db');
Create my own LogWritter similar to the framework
<?php
/**
* Description of LogWritter
*
* #author Ruwantha.Lankathilaka
*/
class LogWritter {
protected $sqliteConnection;
public function __construct($connection) {
$this->sqliteConnection = $connection;
}
/**
* Write function will bypass the slim default LogWriter and will return
* last inserted log id which could be used as a reference
*
* #param type $object will get the error message
* #param type $level will get the error levels of \Slim\Log
* #return mix if successfully logged will return the last insert id, else
* will return false
*/
public function write($object,$level) {
//Determine label
$label = 'DEBUG';
$message = (string) $object;
switch ($level) {
case \Slim\Log::FATAL:
$label = 'FATAL';
break;
case \Slim\Log::ERROR:
$label = 'ERROR';
break;
case \Slim\Log::WARN:
$label = 'WARN';
break;
case \Slim\Log::INFO:
$label = 'INFO';
break;
}
$sqliteQuery = "INSERT INTO logs (lable,message) VALUES (:lable,:message)";
$statement = $this->sqliteConnection->prepare($sqliteQuery);
$result = $statement->execute(array(':lable'=>$label,':message'=>$message));
if(!empty($result)){
return $this->sqliteConnection->lastInsertId();
}else{
return false;
}
}
}
Add the LogWritter to the index
Add the LogWritter to the Slim app
$app = new \Slim\Slim(array(
'log.writer' => $logWriter,
'log.enabled' => true,
'log.level' => \Slim\Log::DEBUG,
'debug' => true
));
now you can get the log from app
$retult = $app->log->error('test error');
$result will have the inserted log id false if the log failed
Hope this will help someone in future.
Related
Hi guys i am trying to create a phpunit test for below function.
/**
* Get file size
*
* #param string $filePath
* #return int
*/
public function getFileSize(string $filePath): int
{
if (!file_exists($filePath)) {
return 0;
}
return filesize($filePath);
}
So far i have tried like this
/**
* Test get file size with invalid data
*/
public function testGetFileSizeWithValidData()
{
$filePath = 'rgreherher';
$service = new Tickets_Service_ContactMomentVehicle();
$result = $service->getFileSize($filePath);
$this->assertSame($result, $filePath);
}
So when i run in my terminal i am getting error as
<string:rgreherher> does not match expected type "integer".
Can anyone help me what mistake i have done.
Thanks in advance.
The error is telling you exactly what is going on, you are comparing an integer ($result) to a string ($filePath).
If I understand your test case correctly, you should replace $filePath with $filePath's size instead.
public function testGetFileSizeWithValidData()
{
$filePath = 'rgreherher';
$filePathSize = 55; // actual file size of $filePath
$service = new Tickets_Service_ContactMomentVehicle();
$result = $service->getFileSize($filePath);
$this->assertSame($result, $filePathSize);
}
In your test you assume that the file exists, but if not you must remember that php function filesize (which is in your function getFileSize) returns false and generates an E_WARNING if there is no file.
I would like to run a duplicate content check just before firing off an email whcih uses swiftmailer inside my symph2 app to send me dev ERROR log entries.
this functionality sits right next to my error log to database function, where it too has a duplicate check, although that one is much easier, it uses sql.
for this one, i want to maintain the last mail sent body for atleast the next 10 emails sent, so that if my error log goes out of control, it wont keep firing me duplicate emails of the same error.
should i just collect this body onto an object that holds last 10 email bodies, and attach this to the swift mailer class? or is there an easier way, like using something that is already embedded in swift mailer for this kind of post sending use? Or maybe a session..
Edit, i call swift mailer from a backend helper class, so think i can pretty much do anything there so long as its atleast semi-elegant.
EDIT this is a refined version of the method that calls both the persist and firing of email
<?php
class someWierdClass
{
public function addLogAction(Request $request, $persist = TRUE, $addEmail = TRUE)
{
$responseAdd = array();
if ($this->getRequest()->request->all() !== null) {
$data = $this->getRequest()->request->get('data') ? $this->getRequest()->request->get('data') : 'no_data';
$duplicate = $this->getRequest()->request->get('duplicate', null);
}
if ($addEmail) {
$responseAdd[] = 'firedIt';
$this->fireEmailString('You have an error log here. <br>' . $data);
}
if ($persist)
{
$responseAdd[] = 'persistedIt';
$this->persistLog($data, $duplicate);
}
if ($responseAdd)
{
$body = implode(', ', $responseAdd);
return new Response($body);
}
}
}
Log emails in a table and check that there it isn't a duplicate every time you send an email.
To do this, you should create a helper function that queries the emails table for entries who's body matches the body you would like to send. If the query returns nothing, then you know that isn't a duplicate. You would then send the email and log it the database. Otherwise, if it returned (a) record(s), you would send a dev ERROR log entry.
If you would like to only check against the last 10 emails, you would do this by querying for both $body == $new_body and $id >= ($total_rows-10)
You would then inject this into the container and call it using something like this
$this->container->get('helper')->sendEmail($body, $subject, $recipients);
Ok, thanks Dan for the idea as to using the database to do the dup check. If you notice, per your suggestion, i was already doing the dup check, but it made me think. It helped me connect the dots.
What i have done is return the answer if its a duplicate on the response when it does the updating the database, then using that response as a flag to determine if email fires or not. (in my case, i go further to check the updated stamp is at least +1 hour old, as opposed to the 'last 10 emails content' idea)
Heres the code.. Enjoy..
<?php
namespace Acme\AcmeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller,
Acme\AcmeBundle\Entity\Log,
Symfony\Component\HttpFoundation\JsonResponse,
Symfony\Component\HttpFoundation\Response,
Symfony\Component\Config\Definition\Exception\Exception,
Symfony\Component\HttpFoundation\Request,
Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
class someWierdClass
{
/**
* #var array
*/
protected $senderArray = array('no-reply#yorudomain.com' => 'Your Website Name');
/**
* #param Request $request
* #param bool $persist
* #param bool $addEmail
* #return Response
*/
public function addLogAction(Request $request, $persist = TRUE, $addEmail = TRUE)
{
$responseAdd = array();
if ($this->getRequest()->request->all() !== null) {
$data = $this->getRequest()->request->get('data') ? $this->getRequest()->request->get('data') : 'no_data';
$type = $this->getRequest()->request->get('type') ? $this->getRequest()->request->get('type') : 'no_type';
$duplicate = $this->getRequest()->request->get('duplicate', null);
}
if ($addEmail) {
$responseAdd[] = 'firedIt';
$this->fireEmailString('You have an error log here. <br>' . $data);
}
if ($persist) {
$responseAdd[] = 'persistedIt';
$persistResponse = $this->persistLog( $type = $data, $duplicate);
if ($persistResponse) {
// a dup check is done here and results of this is on the response. (e.g. $content->passesCutoff)
$content = json_decode($persistResponse->getContent());
}
}
if ( $addEmail && ( isset($content->passesCutoff) && $content->passesCutoff ))
{
//fire off an email also, because its kind of hard to look in teh logs all the time, sometimes we just want an email.
$successEmail = $this->fireEmailString($data);
if( ! $successEmail )
{
$responseAdd[] = 'firedIt';
}
}
if ($responseAdd) {
$body = implode(', ', $responseAdd);
return new Response($body);
}
}
/**
* #param $emailStringData
* #param null $emailSubject
* #param null $emailTo
* #return mixed
*/
protected function fireEmailString($emailStringData, $emailSubject = null, $emailTo=null){
$templateName = 'AcmeBundle:Default:fireEmailString.html.twig';
if( ! $emailSubject )
{
$emailSubject = 'An email is being fired to you!' ;
}
if( ! $emailTo )
{
$emailTo = 'youremail#gmail.com';
}
$renderedView = $this->renderView(
$templateName, array(
'body' => $emailStringData,
));
$mailer = $this->get('mailer');
$message = $mailer->createMessage()
->setSubject( $emailSubject)
->setBody($emailStringData, 'text/plain')
->addPart($renderedView, 'text/html')
->setFrom($this->senderArray)
->setSender($this->senderArray)
->setTo($emailTo);
$results = $mailer->send($message);
return $results;
}
/**
* #param $type
* #param $data
* #param $duplicate
* #return JsonResponse
*/
protected function persistLog($type, $data, $duplicate) {
$em = $this->getDoctrine()->getManager();
$count = null;
$passesCutoff = null;
$mysqlNow = new \DateTime(date('Y-m-d G:i:s'));
//only two conditions can satisy here, strings '1' and 'true'.
if($duplicate !== '1' && $duplicate !== 'true' /*&& $duplicate != TRUE*/)
{
//in order to check if its unique we need to get the repo
//returns an object (findByData() would return an array)
$existingLog = $em->getRepository('AcmeBundle:Log')->findOneByData(
array('type' => $type, 'data' => $data)
);
if($existingLog)
{
$timeUpdatedString = strtotime($existingLog->getTimeupdated()->format('Y-m-d H:i:s'));
$cutoffStamp = strtotime('+1 hour', $timeUpdatedString); //advance 1 hour (customize this to the amount of time you want to go by before you consider this a duplicate. i think 1 hour is good)
$passesCutoff = time() >= $cutoffStamp ? TRUE : FALSE; //1 hour later
$count = $existingLog->getUpdatedcount();
$existingLog->setUpdatedcount($count + 1); // '2014-10-11 03:52:20' // date('Y-m-d G:i:s')
$em->persist($existingLog);
}
else
{
//this record isnt found, must be unique
$newLog = new Log(); //load our entity
//set in new values
$newLog->setType($type);
$newLog->setData($data);
$newLog->setUpdatedcount(0);
$newLog->setTimeupdated($mysqlNow);
$em->persist($newLog);
}
}
else
{
//we dont care if unique or not, we just want a new row
$newLog = new Log(); //load our entity
$newLog->setType($type);
$newLog->setData($data);
//time updated has been set to auto update to current timestamp in the schema, test first, then remove this
$newLog->setUpdatedcount(0);
$newLog->setTimeupdated($mysqlNow);
$em->persist($newLog);
}
$em->flush();
$response = new JsonResponse();
$response->setData(
array(
'data' => 'persistedIt',
'existingLog' => $count,
'passesCutoff' => $passesCutoff,
));
return $response;
}
}
In hindsight, i would have just passed the last update timestamp back on the response from the persist method, then do the cutoff calculation inside the fire email method obviously, but the above code does work as a starting point.. :-)
The Symfony2 PunkAve FileUpload Bundle works, but because of the returns inside the UploadHandler of BlueImp, it is not possible to get the filename.
<?php
/**
*
* #Route("/upload")
* #Template()
*/
public function uploadAction(Request $request)
{
$editId = $this->getRequest()->get('editId');
if (!preg_match('/^\d+$/', $editId))
{
throw new Exception("Bad edit id");
}
$em = $this->getDoctrine()->getManager();
$entity = $em->getRepository('Foobar:Foobar')->find($editId);
$destinationFolder = 'test';
$fileUploader = $this->get('punk_ave.file_uploader');
$imageName = $fileUploader->handleFileUpload(array('folder' => $destinationFolder ));
$imageEntity = new \Foobar\Entity\Image();
$imageEntity->setImage($imageName);
$imageEntity->setFolder($destinationFolder);
$em->persist($media);
$em->flush();
return true;
}
The example above uploads the image.
The variable $imageName triggers the fileUploadHandler. There is somewhere a return, why it doesn't go the the next lines where it should save the imagename.
How can I still get it working in Symfony? To save the filename in the Entity after he handled the upload?
As they said in documentation: handleFileUpload DOES NOT RETURN as the response is generated in native PHP by BlueImp's UploadHandler class. handleFileUpload has exit(0); at the end so when you call it then entire process stops there. If you want to save files to database you should do it in action which handles request (from documentation's example it will be editAction) and there, again as documentation said, use getFiles to get the list of filenames and mirror that in your database as you see fit.
I am having nested-set (using Gedmo tree) entity called "Location". Entity "Appartment" has location_id and what I need to do it to map scalar value called eg "path" to query that returns all appartments.
In Doctrine1, I had this code:
/**
* Add "path" to each element
*
* #param Doctrine_Query $query
* #param string $separator
*/
protected function addScalar_path(Doctrine_Query $query, $separator=", ")
{
$subquery = "k99.root_id=o.root_id AND k99.lft<=o.lft AND k99.rgt>=o.rgt AND k99.level<=o.level" ;
$query->addSelect("(SELECT GROUP_CONCAT(k99.name ORDER BY k99.level SEPARATOR '$separator') FROM Location k99 WHERE $subquery) AS path") ;
}
Note: "o" alias is used for primary query.
This code would allow me to use
{foreach .... as $appartment}
{$appartment->path}
...
Which would print:
Australia, Victoria, Melbourne, ...other children...
How to do the same thing in D2? And how to even include doctrine extenstions in my symfony2 project?
If you want to use it in QueryBuilder you must :
1) add DQL functions GroupConcat: GroupConcat
2 ) Registering GroupConcat :DQL User Defined Functions
another alternative is to use NativeQuery :Native SQL
In symfony2 registering a function DQL it's very simple just add GROUP_CONCAT in config.yml like:
entity_managers:
default:
dql:
string_functions:
GROUP_CONCAT: YourBundle\Query\Mysql\GroupConcat
For more information visit Registering Custom DQL Functions
If any comes across this post, there's now a Doctrine Extensions repository in Github. The repo has instructions on how to use it. You can use composer to install it and then use any function you're interested in.
EDIT:
For the Sake of the user Tushar, the way to use GROUP_CONCAT in Doctrine2's DQL is simple install Doctrine Extensions:
composer require beberlei/DoctrineExtensions
To enable it: add the following in your config.yml:
doctrine:
orm:
dql:
string_functions:
group_concat: DoctrineExtensions\Query\Mysql\GroupConcat
Then in your code, you should be able now to use Group Concat in your DQLs:
$this->createQueryBuilder('location')
->select('location')
->addSelect("GROUP_CONCAT(DISTINCT location.name SEPARATOR '; ') AS locationNames");
$result = $queryBuilder->getQuery()->getResult();
Or in the case for the original question:
$query->addSelect("(SELECT GROUP_CONCAT(k99.name ORDER BY k99.level SEPARATOR '$separator') FROM Location k99 WHERE $subquery) AS path");
Just an addition to #a.aitboudad's answer: The DQL function GroupConcat linked appears to have a «limited support for GROUP_CONCAT».
Here is the full support version :
The configuration is as he mentioned.
// -------------------------------------------------
// Complete support of GROUP_CONCAT in Doctrine2
// -------------------------------------------------
// Original Article: http://habrahabr.ru/post/181666/
// Automated translation to English: http://sysmagazine.com/posts/181666/
// Original github commit: https://github.com/denisvmedia/DoctrineExtensions/blob/d1caf21cd7c71cc557e60c26e9bf25323a194dd1/lib/DoctrineExtensions/Query/Mysql/GroupConcat.php
/**
* DoctrineExtensions Mysql Function Pack
*
* LICENSE
*
* This source file is subject to the new BSD license that is bundled
* with this package in the file LICENSE.txt.
* If you did not receive a copy of the license and are unable to
* obtain it through the world-wide-web, please send an email
* to kontakt#beberlei.de so I can send you a copy immediately.
*/
namespace DoctrineExtensions\Query\Mysql;
use Doctrine\ORM\Query\AST\Functions\FunctionNode,
Doctrine\ORM\Query\Lexer;
/**
* Full support for:
*
* GROUP_CONCAT([DISTINCT] expr [,expr ...]
* [ORDER BY {unsigned_integer | col_name | expr}
* [ASC | DESC] [,col_name ...]]
* [SEPARATOR str_val])
*
*/
class GroupConcat extends FunctionNode
{
public $isDistinct = false;
public $pathExp = null;
public $separator = null;
public $orderBy = null;
public function parse(\Doctrine\ORM\Query\Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$lexer = $parser->getLexer();
if ($lexer->isNextToken(Lexer::T_DISTINCT)) {
$parser->match(Lexer::T_DISTINCT);
$this->isDistinct = true;
}
// first Path Expression is mandatory
$this->pathExp = array();
$this->pathExp[] = $parser->SingleValuedPathExpression();
while ($lexer->isNextToken(Lexer::T_COMMA)) {
$parser->match(Lexer::T_COMMA);
$this->pathExp[] = $parser->StringPrimary();
}
if ($lexer->isNextToken(Lexer::T_ORDER)) {
$this->orderBy = $parser->OrderByClause();
}
if ($lexer->isNextToken(Lexer::T_IDENTIFIER)) {
if (strtolower($lexer->lookahead['value']) !== 'separator') {
$parser->syntaxError('separator');
}
$parser->match(Lexer::T_IDENTIFIER);
$this->separator = $parser->StringPrimary();
}
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
public function getSql(\Doctrine\ORM\Query\SqlWalker $sqlWalker)
{
$result = 'GROUP_CONCAT(' . ($this->isDistinct ? 'DISTINCT ' : '');
$fields = array();
foreach ($this->pathExp as $pathExp) {
$fields[] = $pathExp->dispatch($sqlWalker);
}
$result .= sprintf('%s', implode(', ', $fields));
if ($this->orderBy) {
$result .= ' ' . $sqlWalker->walkOrderByClause($this->orderBy);
}
if ($this->separator) {
$result .= ' SEPARATOR ' . $sqlWalker->walkStringPrimary($this->separator);
}
$result .= ')';
return $result;
}
}
// -------------------------------------------------
// Example of usage:
// -------------------------------------------------
$query = $this->createQueryBuilder('c')
->select("
c as company,
GroupConcat(b.id, ';', b.headOffice, ';', b.city, ';', s.name
ORDER by b.id
SEPARATOR '|') AS branches
")->leftJoin('c.branches', 'b')
->leftJoin('b.country', 's')
->groupBy('c.id')
->setFirstResult(0)
->setMaxResults(10)
->getQuery();
$result = $query->getResult();
I had similar problem. GROUP_CONCAT does not allow using CASE-WHEN inside.
This library fixed my issues, so maybe it will be helpful.
I asked this question and found out that we can't get the error message thrown by a DataTransformer (according to the only user who answered, maybe it's possible, I don't know).
Anyway, now that I know that, I am stucked with a problem of validation. Suppose my model is this one: I have threads that contains several participants (users).
<?php
class Thread
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="My\UserBundle\Entity\User")
* #ORM\JoinTable(name="messaging_thread_user")
*/
private $participants;
// other fields, getters, setters, etc
}
For thread creation, I want the user to specify the participants usernames in a textarea, separated by "\n".
And I want that if one or more of the usernames specified don't exist, a message is displayed with the usernames that don't exist.
For example, "Users titi, tata and toto don't exist".
For that I created a DataTransformer that transforms the raw text in the textarea into an ArrayCollection containing instances of users. Since I can't get the error message provided by this DataTransformer (such a shame! Is it really impossible?), I don't check the existence of each usernames in the DataTransformer but in the Validator.
Here is the DataTransformer that converts \n-separated user list into an ArrayCollection (so that the DataBinding is ok):
<?php
public function reverseTransform($val)
{
if (empty($val)) {
return null;
}
$return = new ArrayCollection();
// Extract usernames in an array from the raw text
$val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
foreach ($usernames as $username) {
$user = new User();
$user->setUsername($username);
if (!$return->contains($user)) {
$return->add($user);
}
}
return $return;
}
And here is my validator:
<?php
public function isValid($value, Constraint $constraint)
{
$repo = $this->em->getRepository('MyUserBundle:User');
$notValidUsernames = array();
foreach ($value as $user) {
$username = $user->getUsername();
if (!($user = $repo->findOneByUsername($username))) {
$notValidUsernames[] = $username;
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
// Create the list of usernames separated by commas
$list = '';
$i = 1;
foreach ($notValidUsernames as $username) {
if ($i < count($notValidUsernames)) {
$list .= $username;
if ($i < count($notValidUsernames) - 1) {
$list .= ', ';
}
}
$i++;
}
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $list,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
This current implementation looks ugly. I can see the error message well, but the users in the ArrayCollection returned by the DataTransformer are not synchronized with Doctrine.
I got two questions:
Is there any way that my validator could modify the value given in parameter? So that I can replace the simple User instances in the ArrayCollection returned by the DataTransformer into instances retrieved from the database?
Is there a simple and elegant way to do what I'm doing?
I guess the most simple way to do this is to be able to get the error message given by the DataTransformer. In the cookbook, they throw this exception: throw new TransformationFailedException(sprintf('An issue with number %s does not exist!', $val));, if I could put the list of non-existing usernames in the error message, it would be cool.
Thanks!
I am the one that answered your previous thread so maybe someone else will jump in here.
Your code can be simplified considerably. You are only dealing with user names. No need for use objects or array collections.
public function reverseTransform($val)
{
if (empty($val)) { return null; }
// Extract usernames in an array from the raw text
// $val = str_replace("\r\n", "\n", trim($val));
$usernames = explode("\n", $val);
array_map('trim', $usernames);
// No real need to check for dups here
return $usernames;
}
The validator:
public function isValid($userNames, Constraint $constraint)
{
$repo = $this->em->getRepository('SkepinUserBundle:User');
$notValidUsernames = array();
foreach ($userNames as $userName)
{
if (!($user = $repo->findOneByUsername($username)))
{
$notValidUsernames[$userName] = $userName; // Takes care of dups
}
}
if (count($notValidUsernames) == 0) {
return true;
}
// At least one username is not ok here
$invalidNames = implode(' ,',$notValidUsernames);
$this->setMessage(
$this->translator->transChoice(
'form.error.participant_not_found',
count($notValidUsernames),
array(
'%usernames%' => $invalidNames,
'%last_username%' => end($notValidUsernames)
)
)
);
return false;
}
=========================================================================
So at this point
We have used transformer to copy the data from the text area and generated an array of user names during form->bind().
We then used a validator to confirm that each user name actually exists in the database. If there are any that don't then we generate an error message and form->isValid() will fail.
So now we are back in the controller, we know we have a list of valid user names (possibly comma delimited or possibly just an array). Now we want to add these to our thread object.
One way would to create a thread manager service and add this functionality to it. So in the controller we might have:
$threadManager = $this->get('thread.manager');
$threadManager->addUsersToThread($thread,$users);
For the thread manager we would inject our entity manager. In the add users method we would get a reference to each of the users, verify that the thread does not already have a link to this user, call $thread->addUser() and then flush.
The fact that we have wrapped up this sort of functionality into a service class will make things easier to test as we can also make a command object and run this from the command line. it also gives us a nice spot to add additional thread related functionality. We might even consider injecting this manager into the user name validator and moving some of the isValid code to the manager.