My Symfony2 app allows users to upload files. I'd like to users to also be able to download their files.
If I were doing straight PHP, I'd just output the appropriate headers, then output the contents of the file. How would I do this within a Symfony2 controller?
(If you use a hard-coded filename in your answer, that's good enough for me.)
I ended up doing this:
/**
* Serves an uploaded file.
*
* #Route("/{id}/file", name="event_file")
* #Template()
*/
public function fileAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('VNNPressboxBundle:Event')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Event entity.');
}
$headers = array(
'Content-Type' => $entity->getDocument()->getMimeType(),
'Content-Disposition' => 'attachment; filename="'.$entity->getDocument()->getName().'"'
);
$filename = $entity->getDocument()->getUploadRootDir().'/'.$entity->getDocument()->getName();
return new Response(file_get_contents($filename), 200, $headers);
}
Any reason why you do not want to bypass Symfony entirely and just serve the file via your HTTP server (Apache, Nginx, etc)?
Just have the uploaded files dropped somewhere in the document root and let your HTTP server do what it does best.
Update: While the Symfony2 code posted by #Jason Swett will work for 99% of cases - I just wanted to make sure to document the alternative(s). Another way of securing downloads would be to use the mod_secdownload module of Lighttpd. This would be the ideal solution for larger files or files that need to be served quickly with little-as-possible memory usage.
Have a look at the VichUploaderBundle
It will allow you to do this:
/**
* #param integer $assetId
*
* #return Response
*/
public function downloadAssetAction($assetId)
{
if (!$courseAsset = $this->get('crmpicco.repository.course_asset')->findOneById($assetId)) {
throw new NotFoundHttpException('Requested asset (' . $assetId . ') does not exist.');
}
$downloadHandler = $this->get('vich_uploader.download_handler');
return $downloadHandler->downloadObject($courseAsset->getFile(), 'assetFile', null, $courseAsset->getName());
}
Related
I have a section within my site where the user can upload their own profile pictures which is stored in the output directory and tracked in the database like so:
$form = $this->createForm(ProfileUpdateForm::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$user = $this->getUser();
$firstname = $form->get('firstname')->getData();
$lastname = $form->get('lastname')->getData();
$picture = $form->get('profilepicture')->getData();
if($picture == null)
{
$user
->setFirstName($firstname)
->setLastName($lastname);
}
else
{
$originalFilename = pathinfo($picture->getClientOriginalName(), PATHINFO_FILENAME);
// this is needed to safely include the file name as part of the URL
$safeFilename = strtolower(str_replace(' ', '', $originalFilename));
$newFilename = $safeFilename.'-'.uniqid().'.'.$picture->guessExtension();
try {
$picture->move(
'build/images/user_profiles/',
$newFilename
);
} catch (FileException $e) {
$this->addFlash("error", "Something happened with the file upload, try again.");
return $this->redirect($request->getUri());
}
// updates the 'picture' property to store the image file name
// instead of its contents
$user
->setProfilePicture($newFilename)
->setFirstName($firstname)
->setLastName($lastname);
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
$this->addFlash("success", "Your profile was updated!");
return $this->redirectToRoute('account');
}
return $this->render('account/profile.html.twig', [
'profileform' => $form->createView()
]);
That issue I've found is that every time I compile my (local) project, the image is then deleted (because the public/build directory gets built by deleting and creating again).
If I'm not mistaken, isn't that how deployments work too? And if so, is that the right way to upload an image? What's the right way of going about this?
I'm not sure why, but your public/ directory shouldn't be deleted.
If you're using Webpack Encore, then public/build/ content is deleted and created again when you compile assets. But not public/ itself.
For uploads, we create public/upload/ directory.
Then, most of the time, we set some globals, which allow us to save the file name only.
Globals for Twig in config/packages/twig.yaml which "root" will be in your public/ directory
twig:
globals:
app_ul_avatar: '/upload/avatar/'
app_ul_document: '/upload/document/'
And globals for your controllers, repositories, etc in config/services.yaml
parameters:
app_ul_avatar: '%kernel.root_dir%/../public/upload/avatar/'
app_ul_document: '%kernel.root_dir%/../public/upload/document/'
It's handy because, as I just said, you only get to save the file name in the database.
Which mean that, if you got a public/upload/img/ folder, and want to also generates thumbnails, you can then create public/upload/img/thumbnail/ and nothing will change in your database, nor do you have to save an extra path.
Just create a new global app_ul_img_thumbnail, and you're set.
Then all you have to do is call your globals when you need them, and contact with the file name:
In Twig:
{{ app_ul_avatar~dbResult.filename }}
Or in Controller:
$this->getParameter('app_ul_avatar').$dbResult->getFilename();
I would like to open the pdf file in the window browser but I have "The file "\\servername\20\2016080.pdf" does not exist"
If I copy this path in a browser, it's work.
Edit: I have found in the logs
CRITICAL - Uncaught PHP Exception Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException: "The file "\\servername\20\2016080.pdf" does not exist" at C:\wamp64\www\his\vendor\symfony\symfony\src\Symfony\Component\HttpFoundation\File\File.php line 37
Thank you.
$response = new BinaryFileResponse($result = $ServerModel->getDocument($request-> get('id'));
$response->headers->set('Content-Type', 'application/pdf');
return $response;
If you're using symfony 3.2 or later (which you should be), you can use the new file helper to serve binary files.
from the symfony docs
$pdfPath = $this->getParameter('dir.downloads').'/sample.pdf';
return $this->file($pdfPath);
how you go about getting the path of the file may differ depending on your implementation. But if its a straight SplFileInfo:: object php docs then you can just use:
$file->getPathname();
The file helper will automagically do much of heavy lifting for you.
Make sure the file is accessible, either by a route or by an un-firewalled path.
/**
* #Route("/show-pdf", name="show-pdf")
*/
public function showPdf(Request $request) {
ini_set('display_errors', 'On');
$pdf = file_get_contents('path/to/file.pdf');
return new Response($pdf, 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'inline; filename="file.pdf"'
]);
}
MY PROBLEM
I am tying to make stof/StofDoctrineExtensionsBundle work with symfony3. Therefore instead of using $form->bind as listed in example in stof documentation I tried $form->handleRequest. Unluckily after submitting the form I get the following error:
The file "" does not exist
I would be thankful for you advise what am I doing wrong. Maybe somebody would be so kind and provide me with example of working upload scripts?
MY CODE
My Controller code looks like this:
public function plikAction(Request $request, $dok_id, $plik_id)
{
$em = $this->getDoctrine()->getManager();
if($plik_id == "nowy") $plik = new plik();
/.../
$form = $this->createForm(plikType::class, $plik);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
if($plik_id == "nowy")
{
$em->persist($plik);
$uploadableManager = $this->get('stof_doctrine_extensions.uploadable.manager');
$uploadableManager->markEntityToUpload($plik, $plik->getName());
$em->flush();
$this->get('session')->getFlashBag()->add('success','Dodano plik ');
}
/.../
}
form is build using the following function:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('oznaczenie')
->add('name', FileType::class)
->add('wgraj', SubmitType::class);
}
and my entity class:
/**
* #ORM\Entity
* #ORM\Table
* #Gedmo\Uploadable
*/
class plik
{
/**
* #ORM\Column
* #Gedmo\UploadableFileName
*/
private $name;
/*
* #ORM\Column(type="string", length=100)
* #Gedmo\UploadableFilePath
*/
protected $path;
I am using default configuration as listed here https://symfony.com/doc/master/bundles/StofDoctrineExtensionsBundle/index.html.
By default the value of default_file_path is %kernel.root_dir%/../web/uploads.
To make it working with the default configuration,
create the uploads folder in the web directory of your application:
mkdir web/uploads
And make it writable by setting up the correct permissions depending on your web server user.
Assuming www-data is the web server user, use the following:
chown -R www-data:www-data web/uploads
Or change the directory permissions using chmod (at your own risk):
chmod -R 755 web/uploads
(in case of this commands needs to be executed as root, prefix them with sudo)
Hope this solves your problem.
In my case the Symfony3 error:
The file “” does not exist
was connected with PHP not being able write the file down to temporarily folder due to open_basedir restrictions.
Therefore in any case you see this error in Symfony3 make sure that correct permissions are set to folders and that PHP may write data into it.
I followed the Behat 2.5 docs to test mails. After a few tweaks to match Behat 3 I have ended with the following code (I have removed non-relevant parts):
public function getSymfonyProfile()
{
$driver = $this->mink->getSession()->getDriver();
if (!$driver instanceof KernelDriver) {
// Throw exception
}
$profile = $driver->getClient()->getProfile();
if (false === $profile) {
// Throw exception
}
return $profile;
}
/**
* #Then I should get an email with subject :subject on :email
*/
public function iShouldGetAnEmail($subject, $email)
{
$profile = $this->getSymfonyProfile();
$collector = $profile->getCollector('swiftmailer');
foreach ($collector->getMessages() as $message) {
// Assert email
}
// Throw an error if something went wrong
}
When I run this test, it throws the following error:
exception 'LogicException' with message 'Missing default data in Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector' in vendor/symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/DataCollector/MessageDataCollector.php:93
Stack trace:
#0 vendor/symfony/swiftmailer-bundle/Symfony/Bundle/SwiftmailerBundle/DataCollector/MessageDataCollector.php(122): Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector->getMailerData('default')
#1 features/bootstrap/FeatureContext.php(107): Symfony\Bundle\SwiftmailerBundle\DataCollector\MessageDataCollector->getMessages()
My profiler is configured as follows:
# app/config/config_test.yml
framework:
test: ~
profiler:
enabled: true
collect: true
It seems that the Profile is correctly loaded and the MessageDataCollector from Swiftmailer does exist, but it is not doing its work as expected. Any clue to solve this?
Maybe the issue you have has been fixed as I do not have this anymore (I'm using Behat v3.0.15, BrowserKit driver 1.3.* and Symfony v2.6.6).
I managed to reproduce your error but only when I forgot to enable profiler data collecting:
profiler:
collect: false
Once this problem solved (the configuration you provided solving the problem for me) I managed to check emails in my Behat tests.
Two solutions for this:
Solution #1: Intercepting redirects globally
If it does not break all your other tests you can do so by configuring your web profiler as follows:
web_profiler:
intercept_redirects: true
Solution #2: Preventing client to follow redirections temporarily
For my part, intercepting redirections globally in the configuration broke most of my other functional tests. I therefore use this method instead.
As preventing redirections allows mainly to check data in the data collectors I decided to use a tag #collect on each scenario requiring redirect interception. I then used #BeforeScenario and #AfterScenario to enable this behaviour only for those scenarios:
/**
* Follow client redirection once
*
* #Then /^(?:|I )follow the redirection$/
*/
public function followRedirect()
{
$this->getDriver()->getClient()->followRedirect();
}
/**
* Restore the automatic following of redirections
*
* #param BeforeScenarioScope $scope
*
* #BeforeScenario #collect
*/
public static function disableFollowRedirects(BeforeScenarioScope $scope)
{
$context = $scope->getEnvironment()->getContext(get_class());
$context->getDriver()->getClient()->followRedirects(false);
}
/**
* Restore the automatic following of redirections
*
* #param AfterScenarioScope $scope
*
* #AfterScenario #collect
*/
public static function restoreFollowRedirects(AfterScenarioScope $scope)
{
$context = $scope->getEnvironment()->getContext(get_class());
$context->getDriver()->getClient()->followRedirects(true);
}
It's not the answer your are looking for, but I'm pretty sure it will suits your needs (perhaps more).
If I can suggest, try using Mailcatcher with this bundle: https://packagist.org/packages/alexandresalome/mailcatcher
You'll be able to easily tests if emails are sent, what's their subject, follow a link in the body, and so on...
Many steps are included with this bundle.
I'm using just the framework without the CMS module for the first time. When I visit the app via a URL that is not handled by a controller/action, I just get a page with the text "No URL rule was matched". This results from Director::handleRequest() not matching any controllers to the url segments... Or "Action 'ABC' isn't available on class XYZController."
I'd like to direct any unmached requests to a controller equivalent of a nice 404 page. What is the correct or best way to do this?
The error templates are only included in the CMS. The framework just returns the HTTP response code with a message in plain text.
I've just started on my own framework-only project too and this is my solution:
[routes.yml]
---
Name: rootroutes
---
Director:
rules:
'': 'MyController'
'$URLSegment': 'MyController'
[MyController]
class MyController extends Controller {
private static $url_handlers = array(
'$URLSegment' => 'handleAction',
);
public function index() {
return $this->httpError(404, "Not Found");
}
/**
* Creates custom error pages. This will look for a template with the
* name ErrorPage_$code (ie ErrorPage_404) or fall back to "ErrorPage".
*
* #param $code int
* #param $message string
*
* #return SS_HTTPResponse
**/
public function httpError($code, $message = null) {
// Check for theme with related error code template.
if(SSViewer::hasTemplate("ErrorPage_" . $code)) {
$this->template = "ErrorPage_" . $code;
} else if(SSViewer::hasTemplate("ErrorPage")) {
$this->template = "ErrorPage";
}
if($this->template) {
$this->response->setBody($this->render(array(
"Code" => $code,
"Message" => $message,
)));
$message =& $this->response;
}
return parent::httpError($code, $message);
}
}
[ErrorPage.ss]
<h1>$Code</h1>
<p>$Message</p>
You can also create more specific error templates using ErrorPage_404.ss, ErrorPage_500.ss etc.
Without updating the routes as previously mentioned, there's a module that I've been recently working on which will allow regular expression redirection mappings, hooking into a page not found (404). This has been designed to function with or without CMS present :)
https://github.com/nglasl/silverstripe-misdirection
It basically makes use of a request filter to process the current request/response, appropriately directing you towards any mappings that may have been defined.