I am trying to link my local users stored in the database with some external services, so they can login anywhere with the same credentials.
I have an EventListener waiting for some FOSUserEvents like FOSUserEvents::REGISTRATION_SUCCESS or FOSUserEvents::CHANGE_PASSWORD_SUCCESS, fired when the data are valid but not actually saved, to perform some call to the various external services and replicate the new user credentials.
If the services return a message saying that the data are replicated, everything work fine.
But if a service say that there is a problem, no mater what it is, I would like to interrupt the saving process of the form and adding an error message.
The objective is to prevent Symfony to save the data, even if they are valid, if an external service say no, and I don't know how to perform this kind of emergency stop.
Actually, it's only based on FOSUserBundle but if I find a working solution, I will have to execute something similar for other entities, that why I try to be as generic as possible.
Here a some of my code
class MyListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_SUCCESS => 'createUser',
FOSUserEvents::CHANGE_PASSWORD_SUCCESS => 'editUser'
);
}
public function createUser(FormEvent $event)
{
$user = $event->getForm()->getData();
// Check if the user exists before trying to edit
$result = $this->_userExists($user);
if($result['value'] == false)
{
// Create the user
$result = $this->_createUser($user);
// Check if the user is successfully replicated
if($result['result'] != 'success')
{
/*
* Emergency stop (user not replicated)
*/
}
}else{
/*
* Emergency stop (user doesn't exists)
*/
}
}
. . .
}
Actually I didn't find any way to stop the saving workflow and returning to the form page to display an error message. So if somebody has an idea of how to perform this, feel free so write your idea, and thanks you all guys. Stackoverflow is really the best ;-)
Related
I'm trying to write a feature test in RESTful API that I have been created in laravel 8.
I have ChatController that gets user chats by getUserChats method:
// get chats for user
public function getUserChats()
{
$chats = $this->chats->getUserChats();
return ChatShowResource::collection($chats);
}
that called by this route:
Route::get('chats', [
App\Http\Controllers\Chats\ChatController::class, 'getUserChats'
]);
this is my feature test on this route:
public function test_get_user_chats()
{
// creates the fake user
$user = User::factory()->create();
$this->actingAs($user, 'api');
// adds the chat to new fake user that created
$response_create_chat = $this->json('POST', '/api/chats', [
'recipient' => $user->username,
'body' => 'some chat text',
]
);
$response_create_chat
->assertStatus(201)
->assertJsonPath('data.body', 'some chat text');
$response_get_chats = $this->json('get', '/api/chats');
$response_get_chats->assertStatus(200);
}
the test runs successfully by this green output OK (1 test, 3 assertions) but I don't know this is the best way to implementing that.
so the question is that anything else I have to do in my test? am I doing the right feature test?
Generally, you want the scope of your test to be as small as possible. The test case that you provided is currently testing two separate api endpoints:
Creating chats
Getting chats
The issue with this is that if code related to creating chats breaks, your test case related to getting chats will fail.
I would recommend splitting these into two separate test cases. This will allow you to trust that if your test case for getting chats fails, it's as a result of the code relating to getting chats failing rather than code unrelated to this endpoint.
I would also recommend using setUp and tearDown methods to ensure that your test case leaves the database in the same state that it found it. This ensures that other test cases will not have to concern themselves with the data from this test. This means the setUp method should do things such as:
Creating users
Creating chats
The tearDown method would be responsible for deleting the users and chats.
Additionally, you should test the format and data that is output by your endpoint. At the moment you are only checking for a 200 response code. If your api were updated to return a list of users rather than chats, the test case related to getting chats would still pass as it does not actually validate the data that is being returned.
EX:
class GetChatsTest extends TestCase
{
private User $user;
public function testGetChats(): void
{
$chat = $this->json('get', '/api/chats');
// Check the chat's returned value
$this->assertSame('Some parameters', $chat);
}
protected function setUp(): void
{
parent::setUp();
// Create the user in the setup. This is run before each test method is run.
$this->user = User::factory()->create();
// Create the chat for the user (Pseudo code because I don't know how your code works)
$this->user->createChat('Some parameters');
}
protected function tearDown(): void
{
parent::tearDown();
// Delete the user and chats here, pseudocode again. This runs after every test method is run
$this->user->deleteChats();
$this->user->delete();
}
}
One thing to note about the example code is that it does not use the rest api to create the user or the chat but instead uses pseudocode controller methods to ensure that it isn't dependent on the functionality of the chat api for testing.
I know that the general concept behind event sourcing is that the state of the application should be able to be replayed from the event stream.
Sometimes, however, we need to get information for business rules from other parts of the system. i.e. An account has a user. A user has a blacklist status which is required to check if they can access/edit the account.
In the below example (purely for demonstration purposes), a user tries to subtract $10 from their account. If a user has been blacklisted, then we do not want to allow them to remove any funds from the account but we do want to record that they have tried to.
After the request is made, we could query the user model to see if the blacklist exists. If true then we can record it and throw the exception.
The user table/model is currently not event-sourced.
Now when we try to replay the event stream to re-build the projections with the state of the user is not being stored in events, it is no longer possible.
So assuming my current example does not work my questions are:
If we were to move the user into an event stored system (in a different aggregate but all events within the same event-stream) then would it be acceptable to use read models within business rules?
Is there any way we can mix event-sourced and CRUD into the same system when they may depend on each other for business rules.
public function subtractMoney(int $amount)
{
if ($this->accountOwnerIsBlacklisted()){
$this->recordThat(new UserActionBlocked());
throw CouldNotSubtractMoney::ownerBlocked();
}
if (!$this->hasSufficientFundsToSubtractAmount($amount)) {
$this->recordThat(new AccountLimitHit());
if ($this->needsMoreMoney()) {
$this->recordThat(new MoreMoneyNeeded());
}
$this->persist();
throw CouldNotSubtractMoney::notEnoughFunds($amount);
}
$this->recordThat(new MoneySubtracted($amount));
}
private function accountOwnerIsBlacklisted(): bool
{
return $this->accountRepositry()->ownerUser()->isBlackListed();
}
Since you are basically working with DDD (without mentioning it) the answer could lie in the definitions there. In DDD you are supposed to define the boundaries of each aggregate root. Each aggregate root should not store any dependencies to other aggregate roots (The Spatie package doesn't even support it). It should only be made up of the events, which then become the single source of truth.
Given your example, it seems that the blocking of a user is not due to negative events on his account, but rather due to something that happened in relation to his user (account owner). The keyword here seems to be "owner". If you want to store the fact that the user action of trying to withdraw money happened, then you could still apply the event, but the reason would, in this case, come from another aggregate "the user". It doesn't matter if the user itself is event sourced, but the user entity has the method to check if the user is blocked, and therefore it is the business rule in your system that he is not allowed to make withdrawals from the account.
If you cannot model these two together, then I would suggest that you design a domain service which can handle this command. Try to keep them as a part of your model to avoid making your domain model anaemic, if you can.
<?php
class AccountWithdrawalService
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function withdraw($userId, $accountId, $amount)
{
$user = $this->userRepository->find($userId);
// You might inject AccountAggregateRoot too.
$account = AccountAggregateRoot::retrieve($accountId);
if(!$user->isBlackListed())
{
$account->subtractMoney($amount);
}
else
{
// Here we record the unhappy road :-(
$account->moneySubtractionBlocked($amount);
}
$account->persist();
}
}
PS: A further possibility is to inject your userRepository in the actual method handling the withdrawal, as long as the userRepository is not a full dependency of the AccountAggregateRoot. This, I believe, is highly discussed.
Give the Account a UserBlacklistedEvent and use this for validation in the Account. Don't record failed attempts unless thats a reporting requirement.
<?php
class Account {
private bool blacklisted;
function blacklist() {
$this->recordThat(new Blacklisted());
}
public function subtractMoney(int $amount)
{
if ($this->blacklisted) {
throw new DomainException("Why on earth has this backend developer let fella make queries to their account. fk em - wait call security we being attaced");
}
...
<?php
class BlacklistService
{
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
public function blacklist($userId, $accountId)
{
$user = $this->userRepository->find($userId);
// You might inject AccountAggregateRoot too.
$account = AccountAggregateRoot::retrieve($user->accountUuid);
$user->blacklist();
$user->save();
$account->blacklisted(); // no need for userid, in most cases
$account->persist();
}
}
<?php
class AccountCreateService
{
function create() {
$user = $this->userRepository->find($userId);
if ($user->blacklisted) {
throw new DomainException("Why on earth has the frontend developer called this.");
}
$account = AccountAggregateRoot::retrieve($user->accountUuid);
...
Even better: don't throw a Exception in Account AR. just return void or and $this->recordThat(new SubtractionEventOnBlacklistedEvent). Since its invariant is protected. Why throw an event? or if its invarient is protected then the invalid case ins't an exceptional circumstance
The exception after all won't do anything for the user. Its unlightly you wan't them reading the exception screen. And then in this case, you could setup some alternate logging perhaps
im working on an application from my companies previous developer and im at the point now where the boss has asked me to log which user has accessed what page. So I've set up some events and listeners to get the job done as i have been doing for all the other logs but there is a problem.
If i was to fire an event the way I've been doing it up till now id have to re-write the code to fire an event for every separate method that returns a page, but right now this is the only viable option i see because then i can customise the information that i put into the event log (I need the ability to customise information that's being pushed to the event / listener). Example Below
public function showCreateAccount()
{
// NEW EVENT HERE
//I need the account types for this page and the status types
$account_types = AccountController::getTypes();
$status_types = StatusController::getTypes();
$timezone_types = TimezoneController::getTypes();
$currency_types = CurrencyController::getTypes();
return view('create.account')->with('account_types', $account_types)
->with('status_types', $status_types)
->with('currency_types', $currency_types)
->with('timezone_types', $timezone_types);
}
public function showCreateRevenue()
{
// NEW EVENT HERE...
$revenue_types = RevenueController::getTypes();
return view('create.income')->with('revenue_types', $revenue_types);
}
public function showCreateIncomeWithID($id)
{
// NEW EVENT HERE
return view('create.income')->with('account_id', $id);
}
public function showCreateExpense()
{
// NEW EVENT HERE, ETC ETC
$expense_types = ExpenseController::getTypes();
return view('create.outcome')->with('expense_types', $expense_types);
}
Now this will work fine i assume but what im worried about is that this method isnt "good practice" surely there must be an alternative to writing a new event in each and every function whilst still being able to pass relevant information about what page has been accessed?
If i am wrong please correct me if not your answers are welcome
I've been handed off some code that for legacy reasons must keep using the Yii framework. Though when a user comes to a certain page as a guest I need the page to automatically log them in as a certain user. After browsing both the Yii documentation and here I have not found any solutions to this problem. Is what I am trying to do possible with Yii?
I would use Behaviors to do this, probably onBegnRequest. This will fire before every request is handled, and automatically log a user in if they're currently a guest
<?php
class BeginRequest extends CBehavior
{
public function attach($owner)
{
$owner->attachEventHandler('onBeginRequest', array($this, 'handleBeginRequest'));
}
public function handleBeginRequest($event)
{
if(Yii::app()->user->isGuest())
{
$identity = new UserIdentity('known-user', 'known-password');
// authenticate identity or not, up to you if you know which user should be logged in
Yii::app()->user->login($identity);
}
}
While using Sentry in L4, is it possible to make an account be used in multiple computers at the same time? Right now, Sentry logs out the user the moment the same account is used in another computer.
Right now I'm trying for that not to happen and keep both users logged in at the same time. I know that it's a security feature when a user gets logged out, but my project's circumstances aren't what you'd call normal.
Extension to Nico Kaag's answer and implementation of spamoom's comment:
/app/config/packages/cartalyst/sentry/config.php
...
// Modify users array to point to custom model.
'users' => array(
'model' => 'User',
'login_attribute' => 'email',
),
...
/app/models/User.php
use Cartalyst\Sentry\Users\Eloquent\User as SentryUser;
class User extends SentryUser
{
...
...
// Override the SentryUser getPersistCode method.
public function getPersistCode()
{
if (!$this->persist_code)
{
$this->persist_code = $this->getRandomString();
// Our code got hashed
$persistCode = $this->persist_code;
$this->save();
return $persistCode;
}
return $this->persist_code;
}
}
It is possible, but not supported by Sentry itself.
To do this, you have to change some core code in Sentry, or find a way to override the User class that's in the Sentry code.
The function you need to adjust is "GetPresistCode()" in the User model, which can be found in:
/vendor/cartalyst/sentry/src/Cartalyst/Sentry/Users/Eloquent/User.php
And this is how the function should look like (not tested):
/**
* Gets a code for when the user is
* persisted to a cookie or session which
* identifies the user.
*
* #return string
*/
public function getPersistCode()
{
if (!$this->persist_code) {
$this->persist_code = $this->getRandomString();
// Our code got hashed
$persistCode = $this->persist_code;
$this->save();
return $persistCode;
}
return $this->persist_code;
}
I have to say that I highly recommend you don't change the code in Sentry, and that you find another way around, but that might be really hard.