Imagine I have an Article entity, and in this entity have a report attribute which is a json_array type.
Json_array's data like {"key1":"value1","ke2":"value2",...}.
Now I don't know how to use symfony form to render and save these json_array like other normal attribute(e.g.,title).
I searched many articles but I haven't find a clear way to realize it.
class Article
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=255)
*/
private $title;
/**
* #var array
*
* #ORM\Column(name="report", type="json_array")
*/
private $report;
}
Here is my take on it. It sounds like you need a custom form type in a collection and a transformer. The idea is that you create a custom form type to house your key/value pairs, let's call it KeyValueType. Next you want to add this type to your ArticleType using a CollectionType as a wrapper. In order to turn your json to a useable data structure for the form and vice versa a transformer is used.
The KeyValueType class:
class KeyValueType extends AbstractType
{
public function buildForm($builder, $options)
{
$builder
->add('key')
->add('value');
}
}
The ArticleType class:
class ArticleType extends AbstractType
{
public function buildForm($builder, $options)
{
$builder
->add('report', CollectionType::class, [
'entry_type' => KeyValueType::class,
'allow_add' => true,
'allow_delete' => true,
]);
$builder->get('report')
->addModelTransformer(new CallbackTransformer(
function ($reportJson) {
// $reportJson has the structure {"key1":"value1","ke2":"value2",...}
if ($reportJson == null) {
return null;
}
$data = [];
foreach (json_decode($reportJson, true) as $key => $value) {
$data[] = ['key' => $key, 'value' => $value];
}
return $data;
},
function ($reportArray) {
// $reportArray has the structure [ [ 'key' => 'key1', 'value' => 'value1'], [ 'key' => 'key2', 'value' => 'value2'] ]
if ($reportArray == null) {
return null;
}
$data = [];
foreach ($reportArray as $report) {
$data[$report['key']] = $report['value'];
}
return json_encode($data);
}
));
}
}
Related
I am using Laravel 6.20 version with php version 8 get this error when call the user transfomer
here is my code
model class
<?php
namespace App\Models;
use Carbon\Carbon;
use App\Models\Country;
use App\Models\Access\Role;
use App\Models\Admin\Staff;
use App\Models\Admin\Driver;
use App\Models\Admin\Owner;
use App\Models\Request\Request;
use App\Models\Master\Developer;
use App\Models\Master\PocClient;
use App\Models\Traits\HasActive;
use App\Models\Admin\AdminDetail;
use App\Models\Admin\UserDetails;
use App\Models\Payment\UserWallet;
use Laravel\Passport\HasApiTokens;
use App\Models\LinkedSocialAccount;
use App\Models\Payment\DriverWallet;
use App\Base\Services\OTP\CanSendOTP;
use App\Models\Traits\DeleteOldFiles;
use App\Models\Traits\UserAccessTrait;
use Illuminate\Support\Facades\Storage;
use Illuminate\Notifications\Notifiable;
use App\Models\Payment\UserWalletHistory;
use App\Models\Traits\HasActiveCompanyKey;
use App\Models\Traits\UserAccessScopeTrait;
use App\Base\Services\OTP\CanSendOTPContract;
use Nicolaslopezj\Searchable\SearchableTrait;
use Illuminate\Foundation\Auth\User as Authenticatable;
use App\Models\Request\FavouriteLocation;
class User extends Authenticatable implements CanSendOTPContract
{
use CanSendOTP,
DeleteOldFiles,
HasActive,
HasApiTokens,
Notifiable,
UserAccessScopeTrait,
UserAccessTrait,
SearchableTrait,
HasActiveCompanyKey;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'users';
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'name', 'username', 'email', 'password', 'mobile', 'country', 'profile_picture', 'email_confirmed', 'mobile_confirmed', 'email_confirmation_token', 'active','fcm_token','login_by','apn_token','timezone','rating','rating_total','no_of_ratings','refferal_code','referred_by','social_nickname','social_id','social_token','social_token_secret','social_refresh_token','social_expires_in','social_avatar','social_avatar_original','social_provider','company_key','lang'
];
/**
* The attributes that should be hidden for arrays.
*
* #var array
*/
protected $hidden = [
'password', 'remember_token', 'email_confirmation_token',
];
/**
* The attributes that have files that should be auto deleted on updating or deleting.
*
* #var array
*/
public $deletableFiles = [
'profile_picture',
];
/**
* The attributes that can be used for sorting with query string filtering.
*
* #var array
*/
public $sortable = [
'id', 'name', 'username', 'email', 'mobile', 'profile_picture', 'last_login_at', 'created_at', 'updated_at',
];
/**
* The relationships that can be loaded with query string filtering includes.
*
* #var array
*/
public $includes = [
'roles', 'otp','requestDetail'
];
/**
* The accessors to append to the model's array form.
*
* #var array
*/
protected $appends = [
];
/**
* Get the Profile image full file path.
*
* #param string $value
* #return string
*/
// public function getProfilePictureAttribute($value)
// {
// if (empty($value)) {
// $default_image_path = config('base.default.user.profile_picture');
// return env('APP_URL').$default_image_path;
// }
// return Storage::disk(env('FILESYSTEM_DRIVER'))->url(file_path($this->uploadPath(), $value));
// }
public function getProfilePictureAttribute($value)
{
if (!$value) {
$default_image_path = config('base.default.user.profile_picture');
return env('APP_URL').$default_image_path;
}
return Storage::disk(env('FILESYSTEM_DRIVER'))->url(file_path($this->uploadPath(), $value));
}
/**
* Override the "boot" method of the model.
*
* #return void
*/
public static function boot()
{
parent::boot();
// Model event handlers
}
/**
* Set the password using bcrypt hash if stored as plaintext.
*
* #param string $value
*/
public function setPasswordAttribute($value)
{
$this->attributes['password'] = (password_get_info($value)['algo'] === 0) ? bcrypt($value) : $value;
}
/**
* The roles associated with the user.
*
* #return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function roles()
{
return $this->belongsToMany(Role::class, 'role_user', 'user_id', 'role_id');
}
/**
* The OTP associated with the user's mobile number.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function otp()
{
return $this->hasOne(MobileOtp::class, 'mobile', 'mobile');
}
/**
* Get the user model for the given username.
*
* #param string $username
* #return \Illuminate\Database\Eloquent\Model|null|static
*/
public function findForPassport($username)
{
return $this->where($this->usernameField($username), $username)->first();
}
/**
* Get the username attribute based on the input value.
* Result is either 'email' or 'mobile'.
*
* #param string $username
* #return string
*/
public function usernameField($username)
{
return is_valid_email($username) ? 'email' : 'mobile';
}
/**
* The default file upload path.
*
* #return string|null
*/
public function uploadPath()
{
return config('base.user.upload.profile-picture.path');
}
/**
* The Staff associated with the user's id.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function admin()
{
return $this->hasOne(AdminDetail::class, 'user_id', 'id');
}
/**
* The Staff associated with the user's id.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function developer()
{
return $this->hasOne(Developer::class, 'user_id', 'id');
}
/**
* The user wallet history associated with the user's id.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function userWalletHistory()
{
return $this->hasMany(UserWalletHistory::class, 'user_id', 'id');
}
/**
* The favouriteLocations associated with the user's id.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function favouriteLocations()
{
return $this->hasMany(FavouriteLocation::class, 'user_id', 'id');
}
public function userWallet()
{
return $this->hasOne(UserWallet::class, 'user_id', 'id');
}
public function driverWallet()
{
return $this->hasOne(DriverWallet::class, 'user_id', 'id');
}
/**
* The Driver associated with the user's id.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function driver()
{
return $this->hasOne(Driver::class, 'user_id', 'id');
}
public function accounts()
{
return $this->hasMany(LinkedSocialAccount::class, 'user_id', 'id');
}
public function requestDetail()
{
return $this->hasMany(Request::class, 'user_id', 'id');
}
/**
* The Driver associated with the user's id.
*
* #return \Illuminate\Database\Eloquent\Relations\hasOne
*/
public function userDetails()
{
return $this->hasOne(UserDetails::class, 'user_id', 'id');
}
/**
* Get formated and converted timezone of user's created at.
*
* #param string $value
* #return string
*/
public function getConvertedCreatedAtAttribute()
{
if ($this->created_at==null||!auth()->user()->exists()) {
return null;
}
$timezone = auth()->user()->timezone?:env('SYSTEM_DEFAULT_TIMEZONE');
return Carbon::parse($this->created_at)->setTimezone($timezone)->format('jS M h:i A');
}
/**
* Get formated and converted timezone of user's created at.
*
* #param string $value
* #return string
*/
public function getConvertedUpdatedAtAttribute()
{
if ($this->updated_at==null||!auth()->user()->exists()) {
return null;
}
$timezone = auth()->user()->timezone?:env('SYSTEM_DEFAULT_TIMEZONE');
return Carbon::parse($this->updated_at)->setTimezone($timezone)->format('jS M h:i A');
}
/**
* Specifies the user's FCM token
*
* #return string
*/
public function routeNotificationForFcm()
{
return $this->fcm_token;
}
public function routeNotificationForApn()
{
return $this->apn_token;
}
protected $searchable = [
'columns' => [
'users.name' => 20,
'users.email'=> 20
],
];
/**
* The user that the country belongs to.
* #tested
*
* #return \Illuminate\Database\Eloquent\Relations\belongsTo
*/
public function countryDetail()
{
return $this->belongsTo(Country::class, 'country', 'id');
}
public function owner()
{
return $this->hasOne(Owner::class, 'user_id', 'id');
}
}
controller class
class AccountController extends ApiController
{
/**
* Get the current logged in user.
* #group User-Management
* #return \Illuminate\Http\JsonResponse
* #responseFile responses/auth/authenticated_driver.json
* #responseFile responses/auth/authenticated_user.json
*/
public function me()
{
$user = User::where('id', auth()->user()->id)->companyKey()->first();
$user = fractal($user, new UserTransformer)->parseIncludes(['onTripRequest.driverDetail','onTripRequest.requestBill','metaRequest.driverDetail','favouriteLocations']);
}
return $this->respondOk($user);
}
transfomer class
class UserTransformer extends Transformer
{
/**
* Resources that can be included if requested.
*
* #var array
*/
protected $availableIncludes = [
'roles','onTripRequest','metaRequest','favouriteLocations'
];
/**
* Resources that can be included default.
*
* #var array
*/
protected $defaultIncludes = [
'sos'
];
/**
* A Fractal transformer.
*
* #return array
*/
public function transform(User $user)
{
$params = [
'id' => $user->id,
'name' => $user->name,
'username' => $user->username,
'email' => $user->email,
'mobile' => $user->mobile,
'profile_picture' => $user->profile_picture,
'active' => $user->active,
'email_confirmed' => $user->email_confirmed,
'mobile_confirmed' => $user->mobile_confirmed,
'last_known_ip' => $user->last_known_ip,
'last_login_at' => $user->last_login_at,
'rating' => round($user->rating, 2),
'no_of_ratings' => $user->no_of_ratings,
'refferal_code'=>$user->refferal_code,
'currency_code'=>$user->countryDetail->currency_code,
'currency_symbol'=>$user->countryDetail->currency_symbol,
'map_key'=>env('GOOGLE_MAP_KEY'),
'show_rental_ride'=>true,
// 'created_at' => $user->converted_created_at->toDateTimeString(),
// 'updated_at' => $user->converted_updated_at->toDateTimeString(),
];
$referral_comission = get_settings('referral_commision_for_user');
$referral_comission_string = 'Refer a friend and earn'.$user->countryDetail->currency_symbol.''.$referral_comission;
$params['referral_comission_string'] = $referral_comission_string;
return $params;
}
/**
* Include the roles of the user.
*
* #param User $user
* #return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeRoles(User $user)
{
$roles = $user->roles;
return $roles
? $this->collection($roles, new RoleTransformer)
: $this->null();
}
/**
* Include the request of the user.
*
* #param User $user
* #return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeOnTripRequest(User $user)
{
$request = $user->requestDetail()->where('is_cancelled', false)->where('user_rated', false)->where('driver_id', '!=', null)->first();
return $request
? $this->item($request, new TripRequestTransformer)
: $this->null();
}
/**
* Include the request meta of the user.
*
* #param User $user
* #return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeMetaRequest(User $user)
{
$request = $user->requestDetail()->where('is_completed', false)->where('is_cancelled', false)->where('user_rated', false)->where('driver_id', null)->where('is_later', 0)->first();
return $request
? $this->item($request, new TripRequestTransformer)
: $this->null();
}
/**
* Include the request meta of the user.
*
* #param User $user
* #return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeSos(User $user)
{
$request = Sos::select('id', 'name', 'number', 'user_type', 'created_by')
->where('created_by', auth()->user()->id)
->orWhere('user_type', 'admin')
->orderBy('created_at', 'Desc')
->companyKey()->get();
return $request
? $this->collection($request, new SosTransformer)
: $this->null();
}
/**
* Include the favourite location of the user.
*
* #param User $user
* #return \League\Fractal\Resource\Collection|\League\Fractal\Resource\NullResource
*/
public function includeFavouriteLocations(User $user)
{
$fav_locations = $user->favouriteLocations;
return $fav_locations
? $this->collection($fav_locations, new FavouriteLocationsTransformer)
: $this->null();
}
}
but getting error this
Type of App\Transformers\User\UserTransformer::$availableIncludes must be array (as in class League\Fractal\TransformerAbstract)
{
"success": false,
"message": "Type of App\\Transformers\\User\\UserTransformer::$availableIncludes must be array (as in class League\\Fractal\\TransformerAbstract)",
"status_code": 500,
"code": 64,
"debug": {
"line": 15,
"file": "/home/payeazyc/public_html/tagxi/app/Transformers/User/UserTransformer.php",
"class": "Symfony\\Component\\Debug\\Exception\\FatalErrorException",
"trace": [
"#0 {main}"
]
}
}
Since starting from PHP8.* types system become more stricter, and recent version focused on the language design, some of the old principles wont work as you used to them.
In you case, look at the parent class (Transformer::class), there you will find that property $availableIncludes declared with a type of array.
In your child class( UserTransformer::class) this declaration missing on type hint level, and it causes this exception. To fix it simply add a type to your array explicitly (no doc blocs and annotations). the end result will look:
class UserTransformer extends Transformer
{
protected array $availableIncludes = [
'roles','onTripRequest','metaRequest','favouriteLocations'
];
I'm using CakePHP version 4.2 and am noticing some odd behavior from the annotate script that comes bundled with the API. For one component, the annotate script wants to default to the App\ domain that is CakePHP's default. I've changed the application name so most other classes default to the correct application name. But not this one script and so far, only for this one file.
I've included the body of the component, for review, below. You can see that the #method annotation uses the App\ domain. The trouble comes in when I use PHPStan to analyze my code. If I leave the annotation as is, PHPStan will tell me:
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
Line src/Controller/Component/CartManagerComponent.php
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
43 Property Visualize\Controller\Component\CartManagerComponent::$Controller (Visualize\Controller\AppController) does not accept App\Controller\AppController.
44 Call to method loadModel() on an unknown class App\Controller\AppController.
💡 Learn more at https://phpstan.org/user-guide/discovering-symbols
------ --------------------------------------------------------------------------------------------------------------------------------------------------------------
The file itself doesn't use the App\ domain anywhere. I'm not sure where to look for the script to figure out whats wrong. Here is the body of my component in case you see something I do not:
<?php
declare(strict_types=1);
namespace Visualize\Controller\Component;
use Authorization\Identity;
use Cake\Controller\Component;
use Cake\Log\Log;
/**
* CartManager component
*
* #method \App\Controller\AppController getController()
* #property \Visualize\Controller\AppController $Controller
* #property \Visualize\Model\Table\CartsTable $Carts
*/
class CartManagerComponent extends Component
{
/**
* Default configuration.
*
* #var array
*/
protected $_defaultConfig = [];
/**
* #var \Visualize\Controller\AppController
*/
protected $Controller;
/**
* #var \Visualize\Model\Table\CartsTable
*/
protected $Carts;
/**
* #param array $config The current configuration array
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->Controller = $this->getController();
$this->Controller->loadModel('Carts');
}
/**
* Returns the most recent active cart.
*
* #param \Authorization\Identity $user The User entity.
* #return array|\Cake\Datasource\EntityInterface|null
* #noinspection PhpUnnecessaryFullyQualifiedNameInspection
*/
public function getUserCart(Identity $user)
{
$cart = $this->Controller->Carts->newEmptyEntity();
if (!empty($this->Controller->Carts) && is_a($this->Controller->Carts, '\Visualize\Model\Table\CartsTable')) {
$query = $this->Controller->Carts->find('userCart', ['user_id' => $user->getIdentifier()]);
if (!$query->isEmpty()) {
$cart = $query->first();
} else {
$cart->set('user_id', $user->getIdentifier());
$this->Controller->Carts->save($cart);
}
if (is_object($cart) && is_a($cart, '\Cake\Datasource\EntityInterface')) {
$session = $this->Controller->getRequest()->getSession();
$session->write('Cart.id', $cart->id);
}
}
return $cart;
}
/**
* Abandons carts
*
* #param int $user_id The associated user ID
* #param int $cart_id The current cart ID
* #return void
*/
public function pruneCarts(int $user_id, int $cart_id): void
{
if (!empty($this->Controller->Carts) && is_a($this->Controller->Carts, '\Visualize\Model\Table\CartsTable')) {
// Find all the carts we didn't just create:
$userCarts = $this->Controller->Carts->find('all', ['fields' => ['id', 'user_id', 'cart_status']])
->where([
'id !=' => $cart_id,
'user_id' => $user_id,
'cart_status' => 'active',
]);
if (!$userCarts->isEmpty()) {
$count = 0;
foreach ($userCarts as $cart) {
if ($count < 5) {
$record = $this->Controller->Carts->newEmptyEntity();
$record = $this->Controller->Carts->patchEntity($record, $cart->toArray());
$record->set('id', $cart->id);
$record->set('cart_status', ABANDONED_CART);
if (!$this->Controller->Carts->save($record)) {
Log::alert('Error abandoning cart');
}
} else {
$this->Controller->Carts->delete($cart);
}
$count++;
}
}
}
}
}
How do I see validation errors when posting json data and self submitting to a symfony form? isValid() is false but I can't access error messages to return. But the Symfony Profiler DOES show the error messages in the Ajax request history. E.g. when duplicate username the profiler shows:
Validator calls in ValidationListener.php
data.username There is already an account with this username
Forms "registration_form" "App\Form\RegistrationFormType"
There is already an account with this username
Caused by: Symfony\Component\Validator\ConstraintViolation
When all the fields are valid the new User is created in the database successfully as expected.
Here is my controller:
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\LoginFormAuthenticator;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Security\Guard\GuardAuthenticatorHandler;
class RegistrationController extends AbstractController
{
/**
* #Route("/api/register", name="app_register")
*/
public function register(
Request $request,
UserPasswordEncoderInterface $passwordEncoder,
GuardAuthenticatorHandler $guardHandler,
LoginFormAuthenticator $authenticator
): Response {
if ($request->isMethod('POST')) {
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$data = json_decode($request->getContent(), true);
$form->submit($data);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
// login the newly registered user
$login = $guardHandler->authenticateUserAndHandleSuccess(
$user,
$request,
$authenticator,
'main' // firewall name in security.yaml
);
if ($login !== null) {
return $login;
}
return $this->json([
'username' => $user->getUsername(),
'roles' => $user->getRoles(),
]);
} else {
$formErrors = $form->getErrors(true); // returns {}
return $this->json($formErrors, Response::HTTP_BAD_REQUEST);
}
}
}
}
Here is my RegistrationFormType:
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('firstName', TextType::class, [
'label' => 'First Name',
'required' => false
])
->add('lastName', TextType::class, [
'label' => 'Last Name',
'required' => false
])
->add('username')
->add('emailAddress', EmailType::class, [
'label' => 'Email Address'
])
->add('plainPassword', PasswordType::class, [
'mapped' => false
])
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You must comply.',
]),
],
])
->add('Register', SubmitType::class)
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
'csrf_protection' => false
]);
}
public function getName()
{
return 'registration_form';
}
}
Here is my entity:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields={"username"}, message="There is already an account with this username")
* #UniqueEntity(fields={"emailAddress"}, message="There is already an account with this email address")
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $username;
/**
* #ORM\Column(type="json")
*/
private $roles = [];
/**
* #var string The hashed password
* #ORM\Column(type="string")
*/
private $password;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $emailAddress;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $firstName;
/**
* #ORM\Column(type="string", length=80, nullable=true)
*/
private $lastName;
public function getId(): ?int
{
return $this->id;
}
/**
* A visual identifier that represents this user.
*
* #see UserInterface
*/
public function getUsername(): string
{
return (string) $this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
/**
* #see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
/**
* #see UserInterface
*/
public function getPassword(): string
{
return (string) $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
/**
* #see UserInterface
*/
public function getSalt()
{
// not needed when using the "bcrypt" algorithm in security.yaml
}
/**
* #see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getEmailAddress(): ?string
{
return $this->emailAddress;
}
public function setEmailAddress(string $emailAddress): self
{
$this->emailAddress = $emailAddress;
return $this;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
public function setFirstName(?string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
public function setLastName(?string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
}
Got it working.
$formErrors = [];
foreach ($form->all() as $childForm) {
if ($childErrors = $childForm->getErrors()) {
foreach ($childErrors as $error) {
$formErrors[$error->getOrigin()->getName()] = $error->getMessage();
}
}
}
return $this->json(
['errors' => $formErrors],
Response::HTTP_BAD_REQUEST
);
Returns:
{
"errors": {
"username": "There is already an account with this username"
}
}
Basically you get form errors at the top level like if there are extra fields etc but you get the form field errors from the actual child elements. You are only returning the top level form errors.
I have a helper function I use to return all errors from a form if I am returning a JsonResponse.
The following is my extended abstract controller. All my controllers extend this and then I can keep a few helper methods here.
I use the createNamedForm() method and keep the name blank as it's easier when sending from Ajax as I don't need to nest the data in the array with the form name as the key.
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as SymfonyAbstractController;
use Symfony\Component\Form\FormInterface;
class AbstractController extends SymfonyAbstractController
{
protected function createNamedForm(string $name, string $type, $data = null, array $options = []): FormInterface
{
return $this->container->get('form.factory')->createNamed($name, $type, $data, $options);
}
protected function getFormErrors($form)
{
$errors = [];
foreach ($form->getErrors() as $error) {
$errors[] = $error->getMessage();
}
foreach ($form->all() as $childForm) {
if ($childForm instanceof FormInterface) {
if ($childErrors = $this->getFormErrors($childForm)) {
$errors[$childForm->getName()] = $childErrors;
}
}
}
return $errors;
}
}
I would create the form in the following way:
$form = $this->createNamedForm('', RegistrationFormType::class);
and return my form errors as follows:
return $this->json($this->getFormErrors($form), 400)
If you want to use the normal createForm() function then you will need to send the data in the following format:
['registration_form' => ['firstName' => 'Tom', 'lastName' => 'Thumb']]
This works in both Symfony4 and Symfony5
I have the UsersFixture with three records.
The test methods first() and second(), which both are before guest_can_login(), pr's show "Joe", "Joe", as expected. But with test method third(), which comes after guest_can_login(), I get notice error: trying to get property of non-object.
So, for a reason, something in the guest_can_login() breaks the rest of the test methods. I have tried by making a duplicate of guest_can_login() as well.
I think it is strange, as tearDown should "reset" everything after each test. I'm out of ideas. And after reading the Cakephp Testing docs, I haven't been able to solve it.
Any suggestions to help me solve this is much appreciated.
Code below (gist if you prefer: https://gist.github.com/chris-andre/2eb3ad053073caf4f1c81722428a900b):
public $fixtures = [
'app.users',
'app.tenants',
'app.roles',
'app.roles_users',
];
public $Users;
public function setUp()
{
parent::setUp();
$config = TableRegistry::getTableLocator()->exists('Users') ? [] : ['className' => UsersTable::class];
$this->Users = TableRegistry::getTableLocator()->get('Users', $config);
}
/**
* tearDown method
*
* #return void
*/
public function tearDown()
{
unset($this->Users);
TableRegistry::clear();
parent::tearDown();
}
/** #test */
public function guest_can_register()
{
$this->enableCsrfToken();
$this->enableSecurityToken();
$this->configRequest([
'headers' => [
'host' => 'timbas.test'
]
]);
$data = [
'email' => 'chris#andre.com',
'first_name' => 'Christian',
'last_name' => 'Andreassen',
'password' => '123456',
'tenant' => ['name' => 'Test Company AS', 'domain' => 'testcomp', 'active' => true],
'active' => true
];
$this->post('/register', $data);
$this->assertResponseSuccess();
$this->assertRedirect(['controller' => 'Users', 'action' => 'login', '_host' => 'testcomp.timbas.test']);
$user = $this->Users->find()
->contain(['Tenants', 'Roles'])
->where(['Users.email' => 'chris#andre.com'])
->first();
}
/** #test */
public function first()
{
$users = $this->Users->find()->first();
pr($users->first_name);
}
/** #test */
public function second()
{
$users = $this->Users->find()->first();
pr($users->first_name);
}
/** #test */
public function guest_can_login()
{
$this->enableCsrfToken();
$this->enableSecurityToken();
$this->configRequest([
'headers' => [
'host' => 'testcomp.timbas.test'
]
]);
$data = [
'email' => 'chris#andre.com',
'first_name' => 'Christian',
'last_name' => 'Andreassen',
'password' => '123456',
'tenant' => ['name' => 'Test Company AS', 'domain' => 'testcomp', 'active' => true],
'active' => true,
'roles' => ['_ids' => [ADMINISTRATOR_ROLE_ID]]
];
$user = $this->Users->newEntity($data, [
'associated' => ['Tenants', 'Roles']
]);
$this->Users->save($user);
$getNewUser = $this->Users->find()
->contain(['Roles'])
->where(['Users.email' => 'chris#andre.com'])
->first()
->toArray();
// pr($getNewUser->id);
$this->post('/users/login', [
'email' => 'chris#andre.com',
'password' => '123456'
]);
$this->assertSession($getNewUser, 'Auth.User');
}
/** #test */
public function third()
{
$users = $this->Users->find()->first();
pr($users->first_name);
}
EDIT 2018-08-06:
Users::register() is a global context, I cannot be accessed from url with subdomain. E.g. tenant1.domain.com/register will throw a badRequest, while domain.com/register is a valid url. On registration success, user is forwarded to login from right url. Login-url = Tenants.domain + domain + suffix, e.g. tenant1.domain.com. When user is on the tenant scope (url with subdomain), the Tenants.id where Tenants.domain = tenant1, will be added to the where clause in all queries for the models having the behavior attached.
Now, what happens in third() is that the tenant_id from the newly created Tenant in guest_can_login() is added to the query, which means the test "is still on the tenant scope" when third() is run. That is the problem.
The other problem is that setUp() is called on all test methods but third(). testDown() is called on every test methods.
App\Middleware\TenantMiddleware.php:
use InstanceConfigTrait;
/**
* Default config.
* Options:
* - globalScope: tells the middleware what controller and action tenant scope is not being used
* Example
* 'globalScope' => [
* 'Pages' => ['*'], // All actions in PagesController is global
* 'Users' => ['register'] // Register action in UsersController is global
* ]
* #var array
*/
protected $_defaultConfig = [
'globalScope' => [
'Users' => ['register'],
'Landing' => ['*'],
'Pages' => ['*']
],
];
public function __construct($config = [])
{
if (!isset($config['primaryDomain'])) {
$config['primaryDomain'] = Configure::read('Site.domain');
}
$this->setConfig($config);
}
/**
* Invoke method.
*
* #param \Cake\Http\ServerRequest $request The request.
* #param \Psr\Http\Message\ResponseInterface $response The response.
* #param callable $next Callback to invoke the next middleware.
* #return \Psr\Http\Message\ResponseInterface A response
*/
public function __invoke(ServerRequestInterface $request, ResponseInterface $response, $next)
{
// Get subdomains
$subdomains = $request->subdomains();
// If subdomains not empty, the first is always the tenants domain
$subdomain = !empty($subdomains) ? $subdomains[0] : '';
Tenant::setDomain($subdomain);
// Get params of current request
$params = $request->getAttribute('params');
$controller = $params['controller'];
$action = $params['action'];
// Set tenantScope as default
Tenant::setScope('tenant');
$globalScope = $this->getConfig('globalScope');
// If Controller and action is a global scope
if (array_key_exists($controller, $globalScope)) {
if (in_array($action, $globalScope[$controller]) || in_array('*', $globalScope[$controller])) {
Tenant::setScope('global');
}
}
if (
(Tenant::getScope() === 'tenant' && Tenant::tenant() === null)
|| (Tenant::getDomain() === '' && Tenant::getScope() === 'tenant')
|| (Tenant::getDomain() !== '' && Tenant::getScope() === 'global')
) {
throw new NotFoundException('The page you are looking for does not exists.');
}
$primaryDomain = $this->getConfig('primaryDomain');
if (array_key_exists($controller, $globalScope)) {
if (in_array($action, $globalScope[$controller]) && Tenant::getScope() === 'global') {
}
}
return $next($request, $response);
}
App\Model\Behavior\TenantScopeBehavior.php:
protected $_table;
/**
* Default configuration.
*
* #var array
*/
protected $_defaultConfig = [];
public function __construct(Table $table, array $config = [])
{
parent::__construct($table, $config);
}
public function beforeFind(Event $event, Query $query, ArrayObject $options)
{
$model = $this->_table->getAlias();
$foreig_key = 'tenant_id';
if (!isset($options['skipTenantCheck']) || $options['skipTenantCheck'] !== true) {
if (Tenant::getScope() === 'tenant') {
if ($model === 'Tenants') {
$query->where(['Tenants.id' => Tenant::tenant()->id]);
} else {
$query->where([$model . '.' . $foreig_key => Tenant::tenant()->id]);
}
}
}
return $query;
}
public function beforeSave(Event $event, Entity $entity, $options)
{
if (Tenant::getScope() === 'tenant') {
if ($entity->isNew()) {
$entity->tenant_id = Tenant::tenant()->id;
} else {
// Check if current tenant is owner
if ($this->_table->getAlias() === 'Tenants') {
if ($entity->id != Tenant::tenant()->id) {
throw new BadRequestException();
}
} else {
if ($entity->tenant_id != Tenant::tenant()->id) {
throw new BadRequestException();
}
}
}
}
return true;
}
public function beforeDelete(Event $event, Entity $entity, $options)
{
if (Tenant::getScope() === 'tenant') {
if ($entity->tenant_id != Tenant::tenant()->id) { //current tenant is NOT owner
throw new BadRequestException();
}
}
return true;
}
App\Tenant\Tenant.php:
/**
* $_domain will be empty or comtain the domain (subdomain from url)
* #var string can be empty
*/
protected static $_domain;
/**
* $_scope shall be 'global' or 'tenant'.
* #var string
*/
protected static $_scope;
/**
* #var null|object \App\Model\Entity\Tenant
*/
protected static $_tenant;
/**
* Gets domain from $_domain and returns the string
* #return string
*/
public static function getDomain()
{
return self::$_domain;
}
/**
* Set the tenant scope domain. Will be set in the TenantMiddleware, and shall not be set anywhere else
* #param string $domain
* #return string empty or with domain
*/
public static function setDomain($domain)
{
self::$_domain = $domain;
}
/**
* Tenant method
* Return the object \App\Model\Table\Tenants ro null
* #return type
*/
public static function tenant()
{
$tenant = static::_getTenant();
return $tenant;
}
protected static function _getTenant()
{
if (self::$_tenant === null) {
$cachedTenants = Cache::read('tenants');
if($cachedTenants !== false) {
// do something
}
$tenantsTable = TableRegistry::get('Tenants');
$tenant = $tenantsTable->find('all', ['skipTenantCheck' => true])
->where(['Tenants.domain' => self::getDomain()])
->where(['Tenants.active' => true])
->first();
self::$_tenant = $tenant;
}
return self::$_tenant;
}
public static function getScope()
{
return self::$_scope;
}
/**
* Description
* #param type $scope
* #return type
*/
public static function setScope($scope)
{
self::$_scope = $scope;
}
I'm beginner on Symfony2.
I have a Regions-Countries-States-Cities database with more of 2,000,000 results. I have 8 entities:
Region (recursive with itself) - RegionTranslation
Country - CountryTranslation
State (recursive with itself) - StateTranslation
City - CityTranslation
The thing is that when I want to load a countries list (only 250 registers in a pulldown, for example) Symfony+Doctrine load all entities structure (all states of all countries, and all cities of all states, with their respective translations).
I think that it spends a lot of memory.
What's the correct method to do it? Can I load only Country (and translations) with this structure? Any idea?
I have had the same problem for objected that are unassociated. Your best bet is to use select2's ajax loading (http://ivaynberg.github.com/select2/), which will give a limited number of items in the search box, and also narrow searches down by what is typed in the box.
A few things things need to be coded:
A javascript file:
$(document).ready(function(){
$('.select2thing').select2({
minimumInputLength:1
,width: "100%"
,ajax: {
url: <<path>> + "entity/json"
,dataType: 'jsonp'
,quitMillis: 100
,data: function (term, page) {
return {
q: term, // search term
limit: 20,
page: page
};
}
,results: function (data, page) {
var more = (page * 20) < data.total;
return { results: data.objects, more: more };
}
}
});
}
A jsonAction in the controller:
/**
* Lists all Thing entities return in json format
*
*/
public function jsonAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$rep = $em->getRepository('yourBundle:Thing');
$qb = $rep->createQueryBuilder('e');
$limit = $request->query->get('limit');
$current = $request->query->get('current');
$page=$request->query->get('page');
$queries=$request->query->get('q');
$qarray=explode(",", $queries);
$entities=$rep->getJSON($qarray, $page, $limit);
$total=$rep->getJSONCount($qarray);
$callback=$request->query->get('callback');
return $this->render('yourBundle:Thing:json.html.twig'
, array(
'entities' => $entities
,'callback' => $callback
,'total' => $total
)
);
}
A twig template (json.html.twig, possibly customized to display more)
{{callback}}(
{ "objects" :
[
{% for entity in entities %}
{ "id": "{{entity.id}}", "text": "{{entity}}""}
{% if not loop.last %},{% endif %}
{% endfor %}
],
"total": {{total}}
}
)
A transformer:
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Common\Persistence\ObjectManager;
use yourBundle\Entity\Thing;
class ThingTransformer implements DataTransformerInterface
{
/**
* #var ObjectManager
*/
private $em;
/**
* #param ObjectManager $em
*/
public function __construct(ObjectManager $em)
{
$this->em = $em;
}
/**
* Transforms an object (thing) to a string (id).
*
* #param Issue|null $thing
* #return string
*/
public function transform($thing)
{
if (null === $thing) {return "";}
if (is_object($thing) && "Doctrine\ORM\PersistentCollection"==get_class($thing)){
$entity->map(function ($ob){return $ob->getId();});
return implode(",",$thing->toArray());
}
return $thing;
}
/**
* Transforms a string (id) to an object (thing).
*
* #param string $id
* #return Issue|null
* #throws TransformationFailedException if object (thing) is not found.
*/
public function reverseTransform($id)
{
if (!$id) {
return null;
}
//if (is_array($id)){
$qb=$this->em
->getRepository('yourBundle:Thing')
->createQueryBuilder('t');
$thing=$qb->andWhere($qb->expr()->in('t.id', $id))->getQuery()->getResult();
if (null === $entity) {
throw new TransformationFailedException(sprintf(
'A thing with id "%s" does not exist!',
$id
));
}
return $thing;
}
}
Your Controller using the select2 control will have to pass the 'em' to the form builder:
$editForm = $this->createForm(new ThingType()
,$entity
,array(
'attr' => array(
'securitycontext' => $sc
,'em' => $this->getDoctrine()
->getEntityManager()
)
)
);
And in your form type:
if (isset($options['attr']['em'])){ $em = $options['attr']['em'];} else {$em=null;}
$transformer = new ThingTransformer($em);
$builder->add(
$builder->create('thing'
,'hidden'
,array(
'by_reference' => false
,'required' => false
,'attr' => array(
'class' => 'select2thing'
)
)
)
->prependNormTransformer($transformer)
);
You can try to change the hydration mode, using array consumes less memory than creating objects.
Other way you can achieve this is using iterations to avoid memory problems:
Finally I think you can't load all without spending a lot of time and memory, so, why not make several queries to load the whole data?