How use retryable jobs in yii 2 queue AMQP Interop (rabbitmq) ?
It is in the documentation.
You can configure "queue" component for your application
'queue' => [
'class' => \yii\queue\<driver>\Queue::class,
'ttr' => 5 * 60, // Max time for job execution
'attempts' => 3, // Max number of attempts
],
Also, you can override config for a specific Job Model
class SomeJob extends BaseObject implements RetryableJobInterface {
public function execute($queue) {
//...
}
public function getTtr() {
return 15 * 60;
}
public function canRetry($attempt, $error) {
return ($attempt < 5) && ($error instanceof TemporaryException);
}
}
To consume a queue you need worker(s).
Simply you can start a worker with
./yii queue/listen
command
Related
I'm trying to validate form input in yii2, I want to check user's input against a certain value from the database.
In my model i have this
use backend\models\RealAccount;
class Amount extends Model
{
public $amount;
/**
* {#inheritdoc}
*/
public function rules()
{
return [
[['amount'], 'number'],
['amount', 'checkBalance'],
['amount', 'compare', 'compareValue' => function($model) {
return RealAccount::getAccountBalance();
}, 'operator' => '<=', 'message' => \Yii::t('app','Insufficient balance')
],
];
}
public function checkBalance($attribute)
{
/*check wallet balance is greater or equal to amount*/
$balance = RealAccount::getAccountBalance();
if(!$balance >= $this->amount){
$this->addError($attribute, \Yii::t('app','Insufficient balance');
}
}
}
I tried both checkBlance, and also try compareValue but non of this work as I expected. NOTE i didn't use bot the same time, tired one it failed then i try the other,
Suppose my Balance is 200, if user input 201, i want to return error message. thanks for any help
I'm working on a marketing application that allows users to message their contacts. When a message is sent, a new "processed_message" database entry is created. There is a list view that displays all campaigns and the number of messages sent, blocked and failed for each campaign. My problem is that this list view takes way too long to load after there are > 50 campaigns with lots of messages.
Currently each campaign has 3 computed attributes (messages_sent, messages_failed and messages_blocked) that are all in the Campaign model's "appends" array. Each attribute queries the count of processed_messages of the given type for the given campaign.
namespace App;
class Campaign
{
protected $appends = [
'messages_sent',
'messages_blocked',
'messages_failed'
];
/**
* #relationship
*/
public function processed_messages()
{
return $this->hasMany(ProcessedMessage::class);
}
public function getMessagesSentAttribute()
{
return $this->processed_messages()->where('status', 'sent')->count();
}
public function getMessagesFailedAttribute()
{
return $this->processed_messages()->where('status', 'failed')->count();
}
public function getMessagesBlockedAttribute()
{
return $this->processed_messages()->where('status', 'blocked')->count();
}
}
I also tried to query all of the messages at once or in chunks to reduce the number of queries but getting all of the processed_messages for a campaing at once will overflow memory and the chunking method is way too slow since it has to use offset. I considered using eager loading the campaigns with processed_messages but that would obviously use way too much memory as well.
namespace App\Http\Controllers;
class CampaignController extends Controller
{
public function index()
{
$start = now();
$campaigns = Campaign::where('user_id', Auth::user()->id)->orderBy('updated_at', 'desc')->get();
$ids = $campaigns->map(function($camp) {
return $camp->id;
});
$statistics = ProcessedMessage::whereIn('campaign_id', $ids)->select(['campaign_id', 'status'])->get();
foreach($statistics->groupBy('campaign_id') as $group) {
foreach($group->groupBy('status') as $messages) {
$status = $messages->first()->status;
$attr = "messages_$status";
$campaign = $campaigns->firstWhere('id', $messages->first()->campaign_id);
$campaign->getStatistics()->$attr = $status;
}
}
return view('campaign.index', [
'campaigns' => $campaigns
]);
}
}
My main goal is to reduce the current page load time considerably (which can take anywhere from 30 seconds to 5 minutes when there are a bunch of campaigns).
You could use the withCount method to count all the objects without loading the relation.
Reference:
If you want to count the number of results from a relationship without actually loading them you may use the withCount method, which will place a {relation}_count column on your resulting models.
In your controller you could do this:
$count = Campaign::withCount(['processed_messages' => function ($query) {
$query->where('content', 'sent');
}])->get();
You could do multiple counts in the same relationship too:
$campaigns = Campaign::withCount([
'processed_messages',
'processed_messages as sent_message_count' => function ($query) {
$query->where('content', 'sent');
}],
'processed_messages as failed_message_count' => function ($query) {
$query->where('status', 'failed');
}],
'processed_messages as blocked_message_count' => function ($query) {
$query->where('status', 'blocked');
}])->get();
You can access the count with this:
echo $campaigns[0]->sent_message_count
Docs
I specify that I start with Symfony. I want to create an API (without FOSRestBundle) with a token as a means of authentication.
I followed different tutorials for this set up. What I would like is when there is an error that is picked up by the class "AuthTokenAuthenticator", it is returned a json and not a html view.
Here are my script:
AuthTokenAuthenticator
namespace AppBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\PreAuthenticatedToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Http\Authentication\SimplePreAuthenticatorInterface;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\HttpFoundation\JsonResponse;
class AuthTokenAuthenticator implements
SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface
{
const TOKEN_VALIDITY_DURATION = 12 * 3600;
protected $httpUtils;
public function __construct(HttpUtils $httpUtils)
{
$this->httpUtils = $httpUtils;
}
public function createToken(Request $request, $providerKey)
{
//$targetUrlToken = '/auth-tokens'; // login
//$targetUrlUser = '/users/create'; // create account
/*if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrlUser) || $request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrlToken) ) {
return;
}*/
$authTokenHeader = $request->headers->get('X-Auth-Token');
if (!$authTokenHeader) {
//return new JsonResponse(array("error" => 1, "desc" => "INVALID_TOKEN", "message" => "X-Auth-Token header is required"));
throw new BadCredentialsException('X-Auth-Token header is required');
}
return new PreAuthenticatedToken(
'anon.',
$authTokenHeader,
$providerKey
);
}
public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
{
if (!$userProvider instanceof AuthTokenUserProvider) {
throw new \InvalidArgumentException(
sprintf(
'The user provider must be an instance of AuthTokenUserProvider (%s was given).',
get_class($userProvider)
)
);
}
$authTokenHeader = $token->getCredentials();
$authToken = $userProvider->getAuthToken($authTokenHeader);
if (!$authToken || !$this->isTokenValid($authToken)) {
throw new BadCredentialsException('Invalid authentication token');
}
$user = $authToken->getUser();
$pre = new PreAuthenticatedToken(
$user,
$authTokenHeader,
$providerKey,
$user->getRoles()
);
$pre->setAuthenticated(true);
return $pre;
}
public function supportsToken(TokenInterface $token, $providerKey)
{
return $token instanceof PreAuthenticatedToken && $token->getProviderKey() === $providerKey;
}
/**
* Vérifie la validité du token
*/
private function isTokenValid($authToken)
{
return (time() - $authToken->getCreatedAt()->getTimestamp()) < self::TOKEN_VALIDITY_DURATION;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
throw $exception;
}
}
Here is the error return that I have when I do not inform the token:
<!DOCTYPE html>
<html>
<head>
<title> X-Auth-Token header is required (500 Internal Server Error)
How can i do to get a return json response ?
If i try to do a return new JsonResponse(array("test" => "KO")) (simple example), i get this error:
<title> Type error: Argument 1 passed to Symfony\Component\Security\Core\Authentication\AuthenticationProviderManager::authenticate() must be an instance of Symfony\Component\Security\Core\Authentication\Token\TokenInterface, instance of Symfony\Component\HttpFoundation\JsonResponse given, called in /Users/mickaelmercier/Desktop/workspace/api_monblocrecettes/vendor/symfony/symfony/src/Symfony/Component/Security/Http/Firewall/SimplePreAuthenticationListener.php on line 101 (500 Internal Server Error)
You can create your own Error Handler. It's a event listener or subscriber that listens to kernel.exception, when it adds a response to the event, the event propagation is stopped, so the default error handler will not be triggered.
It could look something like this:
<?php declare(strict_types = 1);
namespace App\EventSubsbscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\KernelEvents;
final class ExceptionToJsonResponseSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => 'onKernelException',
];
}
public function onKernelException(GetResponseForExceptionEvent $event): void
{
// Skip if request is not an API-request
$request = $event->getRequest();
if (strpos($request->getPathInfo(), '/api/') !== 0) {
return;
}
$exception = $event->getException();
$error = [
'type' => $this->getErrorTypeFromException($exception),
// Warning! Passing the exception message without checks is insecure.
// This will potentially leak sensitive information.
// Do not use this in production!
'message' => $exception->getMessage(),
];
$response = new JsonResponse($error, $this->getStatusCodeFromException($exception));
$event->setResponse($response);
}
private function getStatusCodeFromException(\Throwable $exception): int
{
if ($exception instanceof HttpException) {
return $exception->getStatusCode();
}
return 500;
}
private function getErrorTypeFromException(\Throwable $exception): string
{
$parts = explode('\\', get_class($exception));
return end($parts);
}
}
ApiPlatform provides its own exception listener like this, that is more advanced, that you could look into if you need "better" exception responses.
I have a Zend Framework 3 MVC app with a controller with two end points. Both need to access the same class. It seems like the best way to do this would be to would be to make an instance of this class a property of the controller class like this:
class IndexController extends AbstractActionController
{
/**
* var Utility $utility
*/
protected $utility;
public function __construct(Utility $utility)
{
$this->utility = $utility;
}
public function indexAction()
{
$this->utility->doA('param1');
return new ViewModel();
}
public function otherAction()
{
$results = $this->utility->validateRequest($this->request);
if ($results)
{
return new ViewModel();
}
else
{
throw new Exception('Invalid request');
}
}
}
However, I don't know how to pass paramaters to the constructor since I don't know where Zend Framework "makes" it.
Zend Framework uses a concept called Dependency Injection. This is based on the D in SOLID, dependency inversion. Theory aside, you need to make a custom factory for your controller in modules.config.php. You also need to make a factory for the class calld Utility.
So first of all, you probably made your project with a command similar to composer create-project -sdev zendframework/skeleton-application. If you did that you probably don't have the latest version of Service Manager. See if the file vendor/bin/generate-factory-for-class exists. If not, execute composer update zendframework/zend-servicemanager to add it there.
Now lets make a factory for the utility class. Lets assume its in module/Application/src/Service/Utility.php and has the namespace Application\Service. You just type vendor/bin/generate-factory-for-class Application\\Service\\Utility > module/Application/src/Service/UtilityFactory.php. If you look in that file you can see:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new Utility();
}
Now lets do the same for the controller with vendor/bin/generate-factory-for-class Application\\Controller\\IndexController > module/Application/src/Controller/IndexControllerFactory.php. Open this factory and see its a little more complex.
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new IndexController($container->get(\Application\Service\Utility::class));
}
$container is your dependency injection container. It executes the __invoke command in these factories when called.
One more thing left to do. you need to edit your module.config.php. Replace the line
'controllers' => [
'factories' => [
Controller\IndexController::class => InvokableFactory::class,
],
],
with
'controllers' => [
'factories' => [
Controller\IndexController::class => Controller\IndexControllerFactory::class,
],
],
Now add the following section to the config:
'service_manager' => [
'factories' => [
Service\Utility::class => InvokableFactory::class,
],
],
Then your controller should work.
I'm worried about Yii2 Rate limiting api?
What is Rate limiting api, why this used?
Here are some methods from Yii2
Can a yii guru explain in simple words about these methods, where and when I should use rate limiting in my api?
public function getRateLimit($request, $action)
{
return [$this->rateLimit, 1]; // $rateLimit requests per second
}
public function loadAllowance($request, $action)
{
return [$this->allowance, $this->allowance_updated_at];
}
public function saveAllowance($request, $action, $allowance, $timestamp)
{
$this->allowance = $allowance;
$this->allowance_updated_at = $timestamp;
$this->save();
}
THE METHODS
getRateLimit(), loadAllowance() and saveAllowance() are three methods contained in the \yii\filters\RateLimitInterface Inteface that the user identity class should implement for enable rate limiting of your api.
getRateLimit() is the first method and it returns the maximum number of api calls that you can do in x seconds:
public function getRateLimit($request, $action) {
return [1,20]; // There can be 1 api call every 20 seconds
}
loadAllowance() return the number of the remaining allowed requests with the corresponding UNIX timestamp of the last time these where checked.
public function loadAllowance($request, $action)
{
return [$this->allowance, $this->allowance_updated_at];
}
saveAllowance() assign to $this->allowance the value of remaining allowed requests and save the timestamp in $this->allowance_updated_at.
public function saveAllowance($request, $action, $allowance, $timestamp)
{
$this->allowance = $allowance; //Saving Remaining Requests
$this->allowance_updated_at = $timestamp; // Saving Timestamp
$this->save(); //Save the model
}
IMPLEMENTATION
This is how implemented the Rate Limiting in my example application (using advanced template):
1 Set the user identity class
In the config/main.php of your api application set the user component.
'user' => [
'identityClass' => 'api\models\User', // User Model for your api
'enableSession' => false,
'loginUrl' => null,
],
2 Create a user model
This is model should implement the \yii\filters\RateLimitInterface:
This is mine:
class User extends \common\models\User implements \yii\filters\RateLimitInterface
{
public $rateLimit = 1;
public $allowance;
public $allowance_updated_at;
public function getRateLimit($request, $action) {
return [$this->rateLimit,1];
}
public function loadAllowance($request, $action)
{
return [$this->allowance, $this->allowance_updated_at];
}
public function saveAllowance($request, $action, $allowance, $timestamp)
{
$this->allowance = $allowance;
$this->allowance_updated_at = $timestamp;
$this->save();
}
}
After these two step Yii will automatically use yii\filters\RateLimiter configured as an action filter for yii\rest\Controller to perform rate limiting check (as cited in the documentation).
The last thing you have to do is disable the Rate limit header in your rest controller behaviors:
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['rateLimiter']['enableRateLimitHeaders'] = false;
return $behaviors;
}
WHEN YOU SHOULD USE RATE LIMITING IN YOUR APPLICATION
Api calls return data (with your filters) from your database so when they're called the server execute queries. More are the calls more are also the number of queries that are execute,
You must limit the number of the calls in order to prevent Server heavy works and a resulting fallout of your system.
Hope this will help.
I'm not going far from the Yii2 Guide, but i don't think i can explain this in a simplier way.
Maybe Yii2 document could help you a lot,and link following,
http://www.yiiframework.com/doc-2.0/guide-rest-rate-limiting.html
you need to alter your user table in Database.The rate limiting takes effect for user loginned
I implemented each step but not show the headers
X-Rate-Limit-Limit, the maximum number of requests allowed with a time period
X-Rate-Limit-Remaining, the number of remaining requests in the current time period
X-Rate-Limit-Reset, the number of seconds to wait in order to get the maximum number of allowed requests
`
you can use rate limithing composer.
add :"ethercreative/yii2-ip-ratelimiter": "1.*"
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['rateLimiter'] = [
// Use class
'class' => RateLimiter::className(),
'rateLimit' => 1,
'timePeriod' => 2,
'separateRates' => false,
'enableRateLimitHeaders' => false,
];
return $behaviors;
}
public function getRateLimit($request, $action) {
$id = $action->getUniqueId();
$limits = [
'user/login' => [20,10],
'article/index' => [100,10],
.......
'other' => [50,10]
];
if(!array_key_existe($id,$limits)) $id = 'other';
return $limits[$id];
}