I'm using Laravel 5.8 with Dropzone.js to upload files to a library, which I'm able to do successfully. I thought it good practise to write a test to verify this but it always fails.
I've seen a similar situation within this question: Laravel dusk: test file upload with Dropzone.js
My controller method looks like this is just called store and it looks like this:
/**
* Store a new library file in the database
*
* #param StoreArticle $request
* #return void
*/
public function store(StoreLibrary $request)
{
$data = $request->validated();
$category = $data['category'];
$files = $data['file'];
foreach ($files as $file) {
$original_name = $file->getClientOriginalName();
$mime_type = $file->getClientOriginalExtension();
$size = $file->getSize();
// Generate a name for this file
$system_generated_name = sha1(date('YmdHis') . str_random(30)) . '.' . $file->getClientOriginalExtension();
// Store the file on the disk 'library'
$path = Storage::disk('library')->putFileAs(null, $file, $system_generated_name);
// Store a reference to this file in the database
Library::create([
'display_name' => $original_name,
'file_name' => $system_generated_name,
'mime_type' => $mime_type,
'size' => $size,
'disk' => $this->disk,
'storage_location' => $path,
'category' => $category,
]);
}
// Return a JSON response
return response()->json([
'success' => true,
'file' => [
'original_name' => $original_name,
'generated_name' => $system_generated_name,
'path' => $path,
'size' => $size,
]
], 200);
}
The StoreLibrary class is a FormRequest and looks like this:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreLibrary extends FormRequest
{
/**
* Set allowed extensions for each file category
* This can be appended to as necessary as it's somewhat restrictive
*/
private $image_ext = [
'jpg', 'jpeg', 'png', 'gif', 'ai', 'svg', 'eps', 'ps'
];
private $audio_ext = [
'mp3', 'ogg', 'mpga'
];
private $video_ext = [
'mp4', 'mpeg'
];
private $document_ext = [
'doc', 'docx', 'dotx', 'pdf', 'odt', 'xls', 'xlsm', 'xlsx', 'ppt', 'pptx', 'vsd'
];
/**
* Merge all listed extensions into one massive array
*
* #return array Extensions of all file types
*/
private function extension_whitelist()
{
return array_merge($this->image_ext, $this->audio_ext, $this->video_ext, $this->document_ext);
}
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'category' => [
'required',
'string'
],
'file.*' => 'required|file|mimes:' . implode(',', $this->extension_whitelist()) . '|max:50000'
];
}
/**
* Get the error messages for the defined validation rules.
*
* #return array
*/
public function messages()
{
return [
'category.required' => 'A category is required when uploading library files.',
'file.*.required' => 'Please select a file to upload',
'file.*.mimes' => 'This type of file is not permitted on the Intranet'
];
}
}
The test I've written looks like this:
/** #test */
public function a_user_with_permission_can_add_files_to_the_library()
{
$this->withoutExceptionHandling();
Storage::fake('library');
$this->setupPermissions();
$user = factory(User::class)->create();
$user->assignRole('admin');
// Assert the uploading an image returns a 200 response
$this->actingAs($user)
->post(route('admin.library.store'), [
'category' => 'Some category',
'file' => UploadedFile::fake()->create("test.jpg", 100)
])->assertStatus(200);
Storage::disk('library')->assertExists("test.jpg");
}
Running the test always returns the following error ErrorException: Undefined index: file however the file input is definitely called file.
The associated blade file section:
<form autocomplete="off">
<div class="form-group">
<label for="category">What category do these files fall into? <span class="required">*</span></label>
<select v-model="category" name="category" id="category" class="form-control form-control--citibase">
<option value="">Select a category</option>
#foreach($categories as $category)
<option value="{{ $category }}">{{ $category }}</option>
#endforeach
</select>
</div>
<div class="form-group">
<vue-dropzone
ref="myVueDropzone"
id="dropzone"
v-bind:options="dropzoneOptions"
v-bind:duplicate-check=true
v-on:vdropzone-sending="sendFile"
v-on:vdropzone-success-multiple="uploadSuccessful"
v-on:vdropzone-queue-complete="queueComplete"
v-on:vdropzone-error="uploadError">
</vue-dropzone>
</div>
<div class="d-flex justify-content-end">
<input type="button" v-on:click="upload_files" class="btn btn-pink" value="Upload files"/>
</div>
</form>
The Vue code:
if (document.getElementById("library-admin")) {
const app = new Vue({
el: "#library-admin",
components: {
vueDropzone: vue2Dropzone
},
data: {
query: "",
category: "",
results: [],
errors: [],
loading: true,
pagination: {},
current_page_url: "",
dropzoneOptions: {
url: "/admin/library",
acceptedFiles: ".jpg, .jpeg, .png, .gif, .svg, .eps, ps, .doc, .docx, .dotx, .pdf, .odt, .xls, .xlsm, .xlsx, .ppt, .pptx, .vsd",
autoProcessQueue: false,
uploadMultiple: true,
parallelUploads: 2,
maxFilesize: 50,
thumbnailWidth: 100,
thumbnailHeight: 100,
dictDefaultMessage: "Drop files here, or click to select them",
addRemoveLinks: true,
headers: {
"x-csrf-token": document
.querySelector('meta[name="csrf-token"]')
.getAttribute("content")
}
}
},
computed: {
show_table() {
return true;
},
has_pagination() {
return true;
}
},
created() {
this.getResults();
},
watch: {
query(after, before) {
this.search();
}
},
methods: {
rename_file(file, index) {
this.$swal({
title: 'Rename file',
input: 'text',
inputValue: file.display_name_excluding_extension,
showCancelButton: true,
inputValidator: (value) => {
if (!value) {
return 'You need to write something!'
}
else if(value){
axios
.patch("/admin/api/library/rename/" + file.id, {
name: value
})
.then(response => {
this.$swal({
type: "success",
title: "File successfully renamed."
});
Vue.set(this.results[index], 'display_name', response.data.display_name);
})
.catch(error => {
this.$swal({
type: "error",
title: "File could not be renamed.",
text: "Please ensure that the file name does not include any dots or special characters."
})
});
}
}
});
},
upload_files() {
this.$refs.myVueDropzone.processQueue();
},
sendFile(file, xhr, formData) {
formData.append("category", this.category);
},
uploadError(file, message, xhr) {
console.log(message.errors);
this.errors = message.errors;
},
uploadSuccessful(files, response) {
this.errors = [];
},
queueComplete(files) {
if (this.errors.length == 0) {
this.getResults();
this.$refs.myVueDropzone.removeAllFiles();
}
},
getResults: function (page_url) {
let vm = this;
page_url = page_url || "/admin/api/library";
this.current_page_url = page_url;
this.results = [];
this.errors = [];
this.loading = true;
axios
.get(page_url)
.then(response => [
vm.makePagination(response.data),
response.data.error ?
(this.error = response.data.error) :
(this.results = response.data.data),
(this.loading = false)
])
.catch((this.error = ""));
},
search: _.debounce(function () {
if (this.query !== "") {
let vm = this;
this.results = [];
this.error = "";
this.loading = true;
axios
.get("/admin/api/library/search", {
params: {
q: this.query
}
})
.then(response => [
vm.makePagination(response.data),
response.data.error ?
(this.errors = response.data.error) :
(this.results = response.data.data),
(this.loading = false)
])
.catch(error => {
console.log(error.response.data.errors);
this.errors = error.response.data.errors;
});
} else {
this.getResults();
}
}, 500),
makePagination: function (data) {
var pagination = {
from: data.from,
to: data.to,
total: data.total,
current_page: data.current_page,
last_page: data.last_page,
next_page_url: data.next_page_url,
prev_page_url: data.prev_page_url
};
this.pagination = pagination;
},
delete_file: function (element) {
if (
confirm(
"Do you really want to delete " + element.display_name
)
) {
axios
.delete("/admin/library/" + element.id)
.then(response => [this.getResults()])
.catch(error => {
console.log(error.response.data.errors);
this.errors = error.response.data.error;
});
}
},
archive_file: function (element) {
axios
.patch("/admin/library/" + element.id)
.then(response => [
element.status == "published" ?
(element.status = "draft") :
(element.status = "published")
])
.catch(error => {
console.log(error.response.data.errors);
this.success = false;
this.errors = error.response.data.errors;
});
}
}
});
}
I managed to find a solution to this issue. It was because file is an array, meaning it needed to have keys and indexes when testing.
This is shown in the code below:
/** #test */
public function a_user_with_permission_can_add_files_to_the_library()
{
$this->withoutExceptionHandling();
Storage::fake('library');
$this->setupPermissions();
$user = factory(User::class)->create();
$user->assignRole('admin');
// Assert the uploading an image returns a 200 response
$this->actingAs($user)
->post(route('admin.library.store'), [
'category' => 'Some category',
'file' => [
0 => UploadedFile::fake()->create("test.jpg", 100),
1 => UploadedFile::fake()->create("test.png", 100),
2 => UploadedFile::fake()->create("test.doc", 100),
3 => UploadedFile::fake()->create("test.ppt", 100),
4 => UploadedFile::fake()->create("test.pdf", 100),
]
])->assertStatus(200);
$this->assertEquals(5, Library::count());
}
Related
i'm new in graphql.
I'm try to config graphql mutation via siler+swoole php framework, that use webonyx/graphql-php.
When i post query i'm get error "Schema is not configured for mutations", but it's configured in my shema.
My index
$typeDefs = file_get_contents(__DIR__.'/schema.graphql');
$resolvers = include __DIR__.'/resolvers.php';
$schema = GraphQL\schema($typeDefs, $resolvers);
GraphQL\execute($schema, GraphQL\request()->toArray(), [], [])
schema.graphql :
schema {
query: Query
mutation: Mutation
}
type Query {
clusters: [Cluster!]!
}
type Cluster {
id: Int
title: String
}
type Mutation {
addCluster(title: String!): Cluster!
}
resolver.php
<?php
use RedBeanPHP\R;
//R::setup('sqlite:'.__DIR__.'/db.sqlite');
$clusters = [
'clusters' => function () {
return R::findAll('clusters');
},
];
$queryType = [
'clusters' => function () {
return R::findAll('clusters');
},
];
$mutationType = [
'addCluter' => function ($root, $args) {
$title = $args['title'];
$cluster = R::dispense('cluster');
$cluster['title'] = $title;
R::store($cluster);
return $cluster;
},
];
return [
'Cluster' => $clusters,
'Query' => $queryType,
'Mutation' => $mutationType,
];
And my query is:
mutation addCluster($clusterName: String) {
addCluster(clusterName: $clusterName) {
id
}
}
The response says:
Schema is not configured for mutations
I am creating a laravel web app on windows server and IIS.
I want to send web push messages to clients and to boost performance I want to use Laravel Queue. Addition queue or work with queue is okay but web push messages don't come.
I am using IIS service on a azure virtual machine. My codes are here.
Thank you.
This is my notification controller :
<?php
namespace App\Http\Controllers\dashboard\notifications;
use App\Http\Controllers\Controller;
use App\Jobs\SendNotifications;
use App\Models\Notifications;
use Illuminate\Http\Request;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;
class indexController extends Controller
{
public function subscription(Request $request)
{
$request->validate([
"endpoint" => "required",
"p256dh" => "required",
"axn" => "required",
"auth" => "required"
]);
if ($request->axn == "subscribe") {
$num = Notifications::where("endpoint", "=", strip_tags($request->endpoint))->count();
if ($num == 0) {
$result = Notifications::create([
"user_id" => auth()->user()->id,
"auth" => $request->auth,
"p256dh" => $request->p256dh,
"endpoint" => strip_tags($request->endpoint)
]);
if ($result) {
$endpoints = Notifications::where("user_id", "=", auth()->user()->id)->get();
foreach ($endpoints as $key => $value){
dispatch(new SendNotifications($value->endpoint,$value->auth,$value->p256dh, __("notifications.New device has been subscribed to our notification service."), __("notifications.New device has been subscribed to our notification service."), LaravelLocalization::localizeUrl('/dashboard')));
}
return response()->json(['success' => true, "data" => "success"], 200);
} else {
return response()->json(['success' => false, "data" => "fail"], 400);
}
} else {
return response()->json(['success' => true, "data" => "success"], 200);
}
} else {
$deletion = Notifications::where("endpoint", "=", strip_tags($request->endpoint))->delete();
if ($deletion) {
return response()->json(['success' => true, "data" => "success"], 200);
} else {
return response()->json(['success' => false, "data" => "fail"], 400);
}
}
}
}
and this is my SendNotification laravel queue :
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;
class SendNotifications implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
private $contentEncoding = "aesgcm";
private $title;
private $message;
private $url;
private $endpoint;
private $image;
private $auth_key;
private $p256dh;
/**
* Create a new job instance.
*
* #return void
*/
public function __construct($endpoint, $auth_key, $p256dh, $title, $message, $url, $image = "")
{
$this->endpoint = $endpoint;
$this->title = $title;
$this->message = $message;
$this->url = $url;
$this->image = $image;
$this->auth_key = $auth_key;
$this->p256dh = $p256dh;
}
/**
* Execute the job.
*
* #return void
*/
public function handle()
{
$auth = [
'VAPID' => [
'subject' => $this->url, // can be a mailto: or your website address
'publicKey' => env("VAPID_PUBLIC_KEY"), // (recommended) uncompressed public key P-256 encoded in Base64-URL
'privateKey' => env("VAPID_PRIVATE_KEY"), // (recommended) in fact the secret multiplier of the private key encoded in Base64-URL
],
];
$defaultOptions = [
'TTL' => 5000, // defaults to 4 weeks
'urgency' => 'normal', // protocol defaults to "normal"
'topic' => 'new_event', // not defined by default,
'batchSize' => 1000, // defaults to 1000
];
$webPush = new WebPush($auth, $defaultOptions);
$webPush->setDefaultOptions($defaultOptions);
$payload = '{"title":"'.$this->title.'","msg":"'.$this->message.'","icon":"https://diginorm.com.tr/images/icons/icon-96x96.png","badge":"https://diginorm.com.tr/images/icons/icon-96x96.png","image":"'.$this->image.'","openUrl":"'.$this->url.'"}';
$subscription = new Subscription($this->endpoint, $this->p256dh, $this->auth_key, $this->contentEncoding);
$webPush->sendOneNotification($subscription, $payload);
}
}
And this is my ServiceWorker.js
self.addEventListener('notificationclick', function(event) {
var notification = event.notification;
var action = event.action;
console.log(notification);
if (action === 'confirm') {
console.log('Confirm was chosen');
notification.close();
} else {
console.log(action);
event.waitUntil(
clients.matchAll()
.then(function(clis) {
var client = clis.find(function(c) {
return c.visibilityState === 'visible';
});
if (client !== undefined) {
client.navigate(notification.data.url);
client.focus();
} else {
clients.openWindow(notification.data.url);
}
notification.close();
})
);
}
});
self.addEventListener('notificationclose', function(event) {
console.log('Notification was closed', event);
});
self.addEventListener('push', function(event) {
console.log('Push Notification received', event);
var data = {title: 'New!', msg: 'Something new happened!', image: '/images/icons/icon-512x512.png', openUrl: '/'};
if (event.data) {
data = JSON.parse(event.data.text());
}
var options = {
body: data.msg,
icon: "/images/icons/icon-96x96.png",
badge: "/images/icons/icon-96x96.png",
image: data.image,
dir: "ltr",
lang: "en-GP",
vibrate:[200, 100, 200, 100, 200, 100, 200],
data: {
url: data.openUrl
}
};
event.waitUntil(
self.registration.showNotification(data.title, options)
);
});
this is jobs table :
and this is the php artisan command result :
I want to make a "two" level dynamic symfony form with this structure:
skillCard: EntityType
session: EntityType
turn: ChoiceType
When I choose the skillCard, I populate the session field, after choosing session I populate the turn field.
At this moment I made this
$builder->add('skillCard', EntityType::class, [
'class' => SkillCard::class,
'choices' => $student->getSkillCards()->filter(function (SkillCard $skillCard) {
return $skillCard->getStatus() != EnumSkillCard::EXPIRED &&
count($skillCard->getSkillCardModulesNotPassed()) > 0;
}),
'placeholder' => ''
]);
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $formEvent) {
/** #var Booking */
$data = $formEvent->getData();
$this->addSessionField($formEvent->getForm(), $data->getSkillCard());
$this->formModifierBySession($formEvent->getForm(), $data->getSession());
}
);
$builder->get('skillCard')->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $formEvent) {
$skillCard = $formEvent->getForm()->getData();
$this->addSessionField($formEvent->getForm()->getParent(), $skillCard);
}
);
protected function addSessionField(FormInterface $form, SkillCard $skillCard = null)
{
$builder = $form->getConfig()->getFormFactory()->createNamedBuilder('session', EntityType::class, null, [
'auto_initialize' => false,
'class' => Session::class,
'placeholder' => '',
'query_builder' => function (EntityRepository $er) use ($skillCard) {
$qb = $er->createQueryBuilder('s');
if (is_null($skillCard)) {
$qb->where('s = 0');
} else {
$qb->where('s.certification = :id')
->andWhere('s.subscribeExpireDate > :date')
->setParameter('id', $skillCard->getCertification())
->setParameter('date', new DateTime());
}
return $qb;
},
]);
$builder->addEventListener(
FormEvents::POST_SUBMIT,
function (FormEvent $formEvent) {
$session = $formEvent->getForm()->getData();
$this->formModifierBySession($formEvent->getForm()->getParent(), $session);
}
);
$form->add($builder->getForm());
}
protected function formModifierBySession(FormInterface $form, Session $session = null)
{
$form->add('turn', ChoiceType::class, [
'choices' => is_null($session) ? [] : range(0, $session->getRounds()),
]);
}
This is the javascript code (for ajax calls):
$(document).ready(function () {
const $skillCard = $('#booking_skillCard');
const $form = $('form[name = "booking"]');
$skillCard.change(function () {
sendAjax($skillCard, $form, function (html) {
replaceFormField('#booking_module', html);
replaceFormField('#booking_session', html);
sessionField($form);
});
});
});
function sessionField($form) {
const $session = $('#booking_session');
$session.change(function () {
sendAjax($session, $form, function (html) {
replaceFormField('#booking_turn', html);
});
});
}
function sendAjax($jObj, $form, successCallBack) {
let data = {};
data[$jObj.attr('name')] = $jObj.val();
console.log($form);
$.ajax({
url: $form.prop('action'),
type: $form.prop('method'),
data: data,
success: successCallBack
})
}
function replaceFormField(selector, html) {
$(selector).replaceWith(
$(html).find(selector)
);
}
When I choose the skillCard, no problems, but when I choose the session I get a TrasformationFailedException: "Unable to reverse value for property path "session": The choice "x" does not exist or is not unique", so the turn field is not populated because $session = $formEvent->getForm()->getData(); is null.
Any suggestion is appreciate.
Problem is that nothing is loaded in the municipality field, it goes undefined. In the AJAX code I get the value of the province well. But in the class addMunicipioField.php does not take the value of the $province, it is always nul
I am trying to make a registration form where part of the usual fields (name, nick, password, ...) I also add two dependent fields Municipality and Province.
The codec Controler:
class UserController extends Controller {
private $session;
public function __construct() {
$this->session = new Session();
}
public function registerAction(Request $request) {
if (is_object($this->getUser())) {
return $this->redirect('home');
}
$user = new DbUsuario();
$form = $this->createForm(RegistreUserType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted()) {
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery('SELECT u FROM BackendBundle:DbUsuario u WHERE u.email = :email OR u.nick = :nick')
->setParameter('email', $form->get("email")->getData())
->setParameter('nick', $form->get("nick")->getData());
$user_isset = $query->getResult();
if (count($user_isset) == 0) {
$factory = $this->get("security.encoder_factory");
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword($form->get("password")->getData(), $user->getSalt());
$user->setPassword($password);
$user->setRole("ROLE_USER");
$user->setImagen(null);
$em->persist($user);
$flush = $em->flush();
if ($flush == null) {
$status = "Te has registrado correctamente";
$this->session->getFlashBag()->add("status", $status);
return $this->redirect("login");
} else {
$status = "No te has registrado correctamente";
}
} else {
$status = "Usuario ya esta registrado.";
}
} else {
$status = "No te has registrado correctamente.";
}
$this->session->getFlashBag()->add("status", $status);
}
return $this->render('AppBundle:User:register.html.twig', array(
"form" => $form->createView() # Genera el html del formulario.
));
}
The Entity that creates the form is DbUsuario, which has the idMunicipio field.
/** #var \BackendBundle\Entity\DbMunicipios */
private $idMunicipio;
/**
* Set idMunicipio
* #param \BackendBundle\Entity\DbMunicipio $idMunicipio
* #return DbUsuario
*/
public function setIdMunicipio(\BackendBundle\Entity\DbMunicipio $idMunicipio = null) {
$this->idMunicipio = $idMunicipio;
return $this;
}
/**
* Get idMunicipio
* #return \BackendBundle\Entity\DbMunicipio
*/
public function getIdMunicipio() {
return $this->idMunicipio;
}
Then the Entity Of DbMunicipio that connects with 'province' with :
/** #var \BackendBundle\Entity\DbProvincia */
private $provincia;
/**#param \BackendBundle\Entity\DbProvincia $provincia
* #return DbMunicipio
*/
public function setProvincia(\BackendBundle\Entity\DbProvincia $provincia = null){
$this->provincia = $provincia;
return $this;
}
// And implement this function.
public function __toString(){
return $this->getMunicipio();
}
/**#return \BackendBundle\Entity\DbProvincia */
public function getProvincia(){
return $this->provincia;
}
And the Entity DbProvincia that only has the fields (id (integer), slug (String) and province (String)).
I define the form as follows:
namespace AppBundle\Form;
use ....
class RegistreUserType extends AbstractType {
public function buildForm(FormBuilderInterface $builder, array $options) {
$factory = $builder->getFormFactory();
$builder->add('nombre', TextType::class, array('label' => 'Nombre',
'required' => 'required',
'attr' => array('class' => 'form-nombre form-control')
));
$builder->add('apellido', TextType::class, array('label' => 'Apellido',
'required' => 'required',
'attr' => array('class' => 'form-apellido form-control')
));
$builder->add('nick', TextType::class, array('label' => 'Nick',
'required' => 'required',
'attr' => array('class' => 'form-nick form-control nick-input')
));
$provinSubscriber = new AddProvinciaField($factory);
$builder->addEventSubscriber($provinSubscriber);
$muniSubscriber = new AddMunicipioField($factory);
$builder->addEventSubscriber($muniSubscriber);
$builder->add('email', EmailType::class, array('label' => 'Correo electrónico',
'required' => 'required',
'attr' => array('class' => 'form-email form-control')
));
$builder->add('password', PasswordType::class, array('label' => 'Password',
'required' => 'required',
'attr' => array('class' => 'form-password form-control')
));
$builder->add('Registrarse', SubmitType::class, array("attr" => array("class" => "form-submit btn btn-success")));
}
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults(array(
'data_class' => 'BackendBundle\Entity\DbUsuario'
));
}
public function getBlockPrefix() { return 'backendbundle_dbusuario'; }
}
I define within the AppBundle \ Form \ eventListener \ AddProvinciaField the classes called in the form:
namespace AppBundle\Form\EventListener;
use ....
use BackendBundle\Entity\DbProvincia;
class AddProvinciaField implements EventSubscriberInterface {
private $factory;
public function __construct(FormFactoryInterface $factory) {
$this->factory = $factory;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit'
);
}
private function addProvinciaForm($form, $provincia) {
$form -> add('provincia', EntityType::class, array(
'class' => 'BackendBundle:DbProvincia',
'label' => 'Provincia',
'placeholder' => '_ Elegir _',
'auto_initialize' => false,
'mapped' => false,
'attr'=> array('class' => 'form-provincia form-control provincia-input'),
'query_builder' => function (EntityRepository $repository) {
$qb = $repository->createQueryBuilder('provincia');
return $qb;
}
));
}
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {return;}
$provincia = ($data->getIdMunicipio()) ? $data->getIdMunicipio()->getProvincia() : null ;
$this->addProvinciaForm($form, $provincia);
}
public function preSubmit(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
if (null === $data) { return;}
$provincia = array_key_exists('provincia-input', $data) ? $data['provincia-input'] : null;
$this->addProvinciaForm($form, $provincia);
}
}
And later I define AddMunicipioField.php:
namespace AppBundle\Form\EventListener;
use ....
use BackendBundle\Entity\DbProvincia;
class AddMunicipioField implements EventSubscriberInterface {
private $factory;
public function _construct(FormFactoryInterface $factory) {
$this->factory = $factory;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit'
);
}
private function addMunicipioForm($form, $provincia) {
$form->add('idMunicipio', EntityType::class, array(
'class' => 'BackendBundle:DbMunicipio',
'label' => 'Municipio',
'placeholder' => '_ Elegir _',
'auto_initialize' => false,
'attr'=> array('class' => 'form-municipio form-control municipio-input'),
'query_builder' => function (EntityRepository $repository) use ($provincia) {
$qb = $repository->createQueryBuilder('idMunicipio')
->innerJoin('idMunicipio.provincia', 'provincia');
if ($provincia instanceof DbProvincia) {
$qb->where('idMunicipio.provincia = :provincia')
->setParameter('provincia', $provincia);
} elseif (is_numeric($provincia)) {
$qb->where('provincia.id = :provincia')
->setParameter('provincia', $provincia);
} else {
$qb->where('provincia.provincia = :provincia')
->setParameter('provincia', null);
}
return $qb;
}
));
}
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if (null === $data) { return; }
$provincia = ($data->getIdMunicipio()) ? $data->getIdMunicipio()->getProvincia() : null ;
$this->addMunicipioForm($form, $provincia);
}
public function preSubmit(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if (null === $data) { return; }
$provincia = array_key_exists('provincia_input', $data) ? $data['provincia_input'] : null;
$this->addMunicipioForm($form, $provincia);
}
}
And finally the AJAX request:
$(document).ready(function(){
var $form = $(this).closest('form');
$(".provincia-input").change(function(){
var data = { idMunicipio: $(this).val() };
$.ajax({
type: 'POST',
url: $form.attr('action'),
data: data,
success: function(data) {
for (var i=0, total = data.length; i < total; i++) {
$('.municipio-input').append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
}
}
});
});
});
I added var_dump and alert() in the code. This is the way output.
In this case, the value of province is always null.
addMunicipioField.php
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$provincia = ($data->getIdMunicipio()) ? $data->getIdMunicipio()->getProvincia() : null ;
var_dump('presetdata');
var_dump($provincia);
$this->addMunicipioForm($form, $provincia);
}
AJAX:
$(document).ready(function(){
var $form = $(this).closest('form');
$(".provincia-input").change(function(){
alert($('.provincia-input').val()); // THIS IS CORRECT VALUE, INTEGER.
var data = { idMunicipio: $(this).val() };
$.ajax({
type: 'POST',
url: $form.attr('action'),
data: data,
success: function(data) {
alert(data);
alert(data.length); // THIS IS INCORRECT.
for (var i=0, total = data.length; i < total; i++) {
$('.municipio-input').append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
}
}
});
});
});
Another point of view
The entities are the same.
In this case it works but I must press the send button. How could I do it without pressing the button, that it was automatic change?
The class RegistreUserType extends AbstractType I add the following lines.
$builder -> add('provincia', EntityType::class, array(
'class' => 'BackendBundle:DbProvincia',
'label' => 'Provincia',
'placeholder' => '_ Elegir _',
'auto_initialize' => false,
'mapped' => false,
'attr'=> array('class' => 'form-provincia form-control provincia-input'),
'query_builder' => function (EntityRepository $repository) {
$qb = $repository->createQueryBuilder('provincia');
return $qb;
}
));
$builder->add('idMunicipio', EntityType::class, array(
'class' => 'BackendBundle:DbMunicipio',
'label' => 'Municipio',
'placeholder' => '_ Elegir _',
'auto_initialize' => false,
'mapped' => false,
'attr'=> array('class' => 'form-municipio form-control municipio-input')
));
$builder->addEventSubscriber(new AddMunicipioField());
The new class AddMunicpioField():
class AddMunicipioField implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SUBMIT => 'preSubmit',
FormEvents::PRE_SET_DATA => 'preSetData',
);
}
public function preSubmit(FormEvent $event){
$data = $event->getData();
$this->addField($event->getForm(), $data['provincia']);
}
protected function addField(Form $form, $provincia){
$form->add('idMunicipio', EntityType::class, array(
'class' => 'BackendBundle:DbMunicipio',
'label' => 'Municipio',
'placeholder' => '_ Elegir _',
'auto_initialize' => false,
'mapped' => false,
'attr'=> array('class' => 'form-municipio form-control municipio-input'),
'query_builder' => function(EntityRepository $er) use ($provincia){
$qb = $er->createQueryBuilder('idMunicipio')
->where('idMunicipio.provincia = :provincia')
->setParameter('provincia', $provincia);
return $qb;
}
));
}
Codec Ajax:
$(document).ready(function () {
$('.provincia-input').change(function () {
var $form = $(this).closest('form');
var data = $('.provincia-input').serialize();
$.ajax({
url: $form.attr('action'),
type: 'POST',
data: data,
success: function (data) {
$('.municipio-input').replaceWith($(html).find('.municipio-input'));
}
});
});
});
I did not notice a field or a property called 'select_provincia' in neither your entity, nor the main form, so I will try guessing, that it probably should be called simply 'provincia', as that is the name for both the property in municipality entity and in the form subscriber for municipality. Also in AddMunicipioField.php you should change this code:
if ($provincia instanceof DbProvincia) {
$qb->where('idMunicipio.provincia = :provincia')
>setParameter('provincia', $provincia);
}
to this:
if ($provincia instanceof DbProvincia) {
$qb->where('idMunicipio.provincia = :provincia')
>setParameter('provincia', $provincia->getId());
}
since when querying you will be comparing provincia to the ID of province.
Further more, make sure you have implemented the __toString() method in the municipality entity, so that symfony would know how to convert that object to a string in order to show it in the select list.
Hope this helps :)
Seeing that you have added new information i will update my answer:
Firstly, In the AddMunicipioField.php you still have basically the same error:
the array key is going to be called the same way you name your field, in this case not 'provincia_input', but 'provincia'. You can see the data that was posted to you by calling "dump($data); die;" just before you check if the array key exists (check for a key name "provincia", as you can see the name matches what you have specified when adding the field to the form (AddProvinciaField.php):
$form -> add('provincia', EntityType::class
Another thing I have noticed in the first js snippet you have posted is that in this part of code:
$(".provincia-input").change(function(){
var data = { idMunicipio: $(this).val() };
$.ajax({
type: 'POST',
url: $form.attr('action'),
data: data,
success: function(data) {
for (var i=0, total = data.length; i < total; i++) {
$('.municipio-input').append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
}
}
});
});
you are taking the input from $(".provincia-input") and sending it as a value for a field called "idMunicipio", which in you situation I don't think makes any sense.
Lastly, I will discus the errors that were made in the last piece of the JS you've posted:
$(document).ready(function () {
$('.provincia-input').change(function () {
var $form = $(this).closest('form');
var data = $('.provincia-input').serialize();
$.ajax({
url: $form.attr('action'),
type: 'POST',
data: data,
success: function (data) {
$('.municipio-input').replaceWith($(html).find('.municipio-input'));
}
});
});
});
First of all, class names are not supposed to be used for identifying the fields that you are using. By definition they are supposed to be used multiple time in the document and describe only style, which might lead to some unexpected behaviour as your codebase grows. Please assign proper ID values to the inputs that you are going to be querying and especially replacing so that you could identify them correctly.
Secondly, please refer to the JS code posted in the official Symfony tutorial by following this link. As you can see the proper way to post data back to the server is not by sending a lone property like you are trying to do in this line:
var data = $('.provincia-input').serialize();
but rather by sending the property as a part of the forms data. So as in the tutorial I've posted, please first create an empty data object:
var data = {};
then add the province value to it:
data[$(this).attr('name')] = $(this).val();
Thirdly, this part of code is clearly incorrect:
success: function (data) {
$('.municipio-input').replaceWith($(html).find('.municipio-input'));
}
As you can see the html variable is undefined in that part of code. This of course is because the variable that you are supposed to be using in this case is called data (the response that you have gotten from the server). So please change it to this:
success: function (data) {
$('.municipio-input').replaceWith($(data).find('.municipio-input'));
}
Lastly, if you are still learning SF and web programming, I would like to suggest taking the bottom up approach to advance your programming knowledge instead, since this case is pretty complex and issues that prevented your code from working still require deeper understanding of the technologies you are using. I would personally suggest reading up on HTML attribute usage, Symfony form handling, read up on what data is available to you during each Symfony form event and maybe try using the dumper component of symfony more to debug your code, since var_dump is really a very inefficient way to debug SF code (would have solved many problems for you).
Solved!!
In my form I added the call to the two new classes:
$builder -> addEventSubscriber(new AddMunicipioFieldSubscriber('idMunicipio'));
$builder -> addEventSubscriber(new AddProvinceFieldSubscriber('idMunicipio'));
The firth select is province, this is the class:
class AddProvinceFieldSubscriber implements EventSubscriberInterface {
private $propertyPathToMunicipio;
public function __construct($propertyPathToMunicipio) {
$this->propertyPathToMunicipio = $propertyPathToMunicipio;
}
public static function getSubscribedEvents() {
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit'
);
}
private function addProvinceForm($form, $Province = null) {
$formOptions = array(
'class' => 'BackendBundle:DbProvincia',
'mapped' => false,
'label' => 'Provincia',
'attr' => array(
'class' => 'class_select_provincia',
),
);
if ($Province) {
$formOptions['data'] = $Province;
}
$form->add('provincia', EntityType::class, $formOptions);
}
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$accessor = PropertyAccess::createPropertyAccessor();
$municipio = $accessor->getValue($data, $this->propertyPathToMunicipio);
$provincia = ($municipio) ? $municipio->getIdMunicipio()->getProvincia() : null;
$this->addProvinceForm($form, $provincia);
}
public function preSubmit(FormEvent $event){
$form = $event->getForm();
$this->addProvinceForm($form);
}
}
The second class is Municipi:
class AddMunicipioFieldSubscriber implements EventSubscriberInterface {
//put your code here
private $propertyPathToMunicipio;
public function __construct($propertyPathToMunicipio){
$this->propertyPathToMunicipio = $propertyPathToMunicipio;
}
public static function getSubscribedEvents(){
return array(
FormEvents::PRE_SET_DATA => 'preSetData',
FormEvents::PRE_SUBMIT => 'preSubmit'
);
}
private function addCityForm($form, $province_id){
$formOptions = array(
'class' => 'BackendBundle:DbMunicipio',
'label' => 'Municipio',
'attr' => array(
'class' => 'class_select_municipio',
),
'query_builder' => function (EntityRepository $repository) use ($province_id) {
$qb = $repository->createQueryBuilder('municipio')
->innerJoin('municipio.provincia', 'provincia')
->where('provincia.id = :provincia')
->setParameter('provincia', $province_id)
;
return $qb;
}
);
$form->add($this->propertyPathToMunicipio, EntityType::class, $formOptions);
}
public function preSetData(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
$accessor = PropertyAccess::createPropertyAccessor();
$municipio = $accessor->getValue($data, $this->propertyPathToMunicipio);
$province_id = ($municipio) ? $municipio->getIdMunicipio()->getProvincia()->getId() : null;
$this->addCityForm($form, $province_id);
}
public function preSubmit(FormEvent $event){
$data = $event->getData();
$form = $event->getForm();
$province_id = array_key_exists('provincia', $data) ? $data['provincia'] : null;
$this->addCityForm($form, $province_id);
}
}
The controled add this function:
public function municipioTestAction(Request $request){
$provincia_id = $request->get('provincia_id');
$em = $this->getDoctrine()->getManager();
$provincia = $em->getRepository('BackendBundle:DbMunicipio')->findByProvinceId($provincia_id);
return new JsonResponse($provincia);
}
Where the function findByProvinceId, I create it as a repository of the entity DbMunicipio.
class DbMunicipioRepository extends EntityRepository{
public function findByProvinceId($provincia_id){
$query = $this->getEntityManager()->createQuery("
SELECT muni
FROM BackendBundle:DbMunicipio muni
LEFT JOIN muni.provincia provin
WHERE provin.id = :provincia_id
")->setParameter('provincia_id', $provincia_id);
return $query->getArrayResult();
}
}
And de codec AJAX.
$(document).ready(function () {
$(".class_select_provincia").change(function(){
var data = {
provincia_id: $(this).val()
};
$.ajax({
type: 'POST',
url: URL+'/municipio-test',
data: data,
success: function(data) {
var $muni_selector = $('.class_select_municipio');
alert(data);
$muni_selector.html('<option>Ciudad</option>');
for (var i=0, total = data.length; i < total; i++) {
$muni_selector.append('<option value="' + data[i].id + '">' + data[i].municipio + '</option>');
}
}
});
});
});
I have a problem with multi file upload. I use a bundle LcnFileUploaderBundle.
When I click save my file don't save in correct folder. I use function syncFilesFromTemp. When I change the function to syncFilesToTemp all is OK but save my files in temporary folder. My controller and twig files looks the same as in link.
my controller:
/**
* Edit Uploads for the given entity id
*
* In a real world scenario you might want to check edit permissions
*
* #param Request $request
* #param $entityId
* #return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
*/
public function editAction(Request $request, $entityId)
{
$editId = intval($entityId);
if ($editId < 1000000000) {
throw new \Exception('invalid editId');
}
$fileUploader = $this->container->get('lcn.file_uploader');
// $fileUploader1 = $this->container->get('lcn.file_manager');
$uploadFolderName = $this->getUploadFolderName($editId);
$form = $this->createFormBuilder()
->setAction($this->generateUrl('lcn_file_uploader_demo_edit', array('entityId' => $entityId)))
->setMethod('POST')
->getForm();
if ($request->getMethod() == 'POST') {
$form->submit($request);
if ($form->isValid()) {
$fileUploader->syncFilesFromTemp($uploadFolderName);
return $this->redirect($this->generateUrl('addItemGallery', array('entityId' => $entityId)));
}
} else {
$fileUploader->syncFilesToTemp($uploadFolderName);
}
return $this->render('PortfolioAdminBundle:Gallery:edit.html.twig', array(
'entityId' => $entityId,
'form' => $form->createView(),
'uploadUrl' => $this->generateUrl('lcn_file_uploader_demo_handle_file_upload', array('entityId' => $entityId)),
'uploadFolderName' => $uploadFolderName,
));
}
_
/**
* Store the uploaded file.
*
* In a real world scenario you might probably want to check
* if the user is allowed to store uploads for the given entity id.
*
* Delegates to LcnFileUploader which implements a REST Interface and handles file uploads as well as file deletions.
*
* This action must not return a response. The response is generated in native PHP by LcnFileUploader.
*
* #param Request $request
* #param int $userId
*/
public function handleFileUploadAction($entityId, Request $request)
{
$entityId = intval($entityId);
if ($entityId < 100000000) {
throw new AccessDeniedHttpException('Invalid edit id: '.$entityId);
}
$this->container->get('lcn.file_uploader')->handleFileUpload(array(
'folder' => $this->getUploadFolderName($entityId),
//'max_number_of_files' => 3, //overwrites parameter lcn_file_uploader.max_number_of_files
//'allowed_extensions' => array('jpg'), //overwrites parameter lcn_file_uploader.allowed_extensions
//'sizes' => array('thumbnail' => array('folder' => 'thumbnail', 'max_width' => 100, 'max_height' => 100, 'crop' => true), 'profile' => array('folder' => 'profile', 'max_width' => 400, 'max_height' => 400, 'crop' => true)), //overwrites parameter lcn_file_uploader.sizes
));
}
#
public function handleFileUpload($options = array())
{
if (!isset($options['folder']))
{
throw new \Exception("You must pass the 'folder' option to distinguish this set of files from others");
}
$options = array_merge($this->options, $options);
$allowedExtensions = $options['allowed_extensions'];
// Build a regular expression like /(\.gif|\.jpg|\.jpeg|\.png)$/i
$allowedExtensionsRegex = '/(' . implode('|', array_map(function($extension) { return '\.' . $extension; }, $allowedExtensions)) . ')$/i';
$sizes = (isset($options['sizes']) && is_array($options['sizes'])) ? $options['sizes'] : array();
$tempFilePath = $options['temp_file_base_path'] . '/' . $options['folder'];
$tempWebPath = $options['temp_web_base_path'] . '/' . $options['folder'];
foreach ($sizes as $index => $size)
{
$sizes[$index]['upload_dir'] = $tempFilePath . '/' . $size['folder'] . '/';;
$sizes[$index]['upload_url'] = $tempWebPath . '/' . $size['folder'] . '/';
$sizes[$index]['no_cache'] = true;
}
$uploadDir = $tempFilePath . '/' . $this->getOriginalFolderName() . '/';;
$uploadUrl = $tempWebPath . '/' . $this->getOriginalFolderName() . '/';
foreach ($sizes as $size)
{
#mkdir($size['upload_dir'], 0777, true);
}
#mkdir($uploadDir, 0777, true);
new $this->options['upload_handler_class'](
array(
'file_namers' => $options['file_namers'],
'upload_dir' => $uploadDir,
'upload_url' => $uploadUrl,
'image_versions' => $sizes,
'accept_file_types' => $allowedExtensionsRegex,
'max_number_of_files' => $options['max_number_of_files'],
'max_file_size' => $options['max_file_size'],
));
// Without this Symfony will try to respond; the BlueImp upload handler class already did,
// so it's time to hush up
exit(0);
}
and twig:
{% block body %}
Demo usage of LcnFileUploaderBundle
Edit Uploads of temporary entity {{ entityId }}
{{ form_start(form, { 'attr': { 'id': 'lcn-file-uploader-demo' } }) }}
{{ form_errors(form) }}
{% include 'PortfolioAdminBundle:Theme:lcnFileUploaderWidget.html.twig' with {
'uploadUrl': uploadUrl,
'uploadFolderName': uploadFolderName,
'formSelector': '#lcn-file-uploader-demo'
} %}
{{ form_rest(form) }}
<input type="submit" value="save">
{% endblock %}
routing.yml
lcn_file_uploader_demo_handle_file_upload:
path: /admin/gallery/added-item/file-upload/{entityId}
defaults: { _controller: PortfolioAdminBundle:Gallery:handleFileUpload }
lcn_file_uploader_demo_edit:
path: /admin/gallery/added-item/edit/{entityId}
defaults: { _controller: PortfolioAdminBundle:Gallery:Edit }
lcn_file_uploader_demo_create:
path: /admin/gallery/added-item/add/
defaults: { _controller: PortfolioAdminBundle:Gallery:Create }
addItemGallery:
path: /admin/gallery/added-item/{entityId}
defaults: { _controller: PortfolioAdminBundle:Gallery:AddItem }