1

I'm trying to create a controller for a user to download a membership card as a pass in Apple Wallet. I'm looking to use the PKPass Library.

I've got things set up and working using their example like this:

$pass = new PKPass(Yii::getAlias('path/to/cert.p12'), 'myPassword');
$data = [
  'description' => 'Membership Card',
  // etc as per example: https://github.com/includable/php-pkpass/blob/master/examples/example.php
];

$pass->setData($data);

$pass->create(true);

but get a 'Headers already sent' error when trying to download the pass. I feel like this is because the PKPass class is outputting to the browser before my module has finished processing the request.

So changing the last line to $createdPass = $pass->create(false); and then handling the output is probably what I want to do, but I'm stuck on how to get that $zip response back into my controller as a file to download. Here is the create method:

public function create($output = false)
{
    // Prepare payload
    $manifest = $this->createManifest();
    $signature = $this->createSignature($manifest);

    // Build ZIP file
    $zip = $this->createZip($manifest, $signature);

    // Return pass
    if (!$output) {
        return $zip;
    }

    // Output pass
    header('Content-Description: File Transfer');
    header('Content-Type: application/vnd.apple.pkpass');
    header('Content-Disposition: attachment; filename="' . $this->getName() . '"');
    header('Content-Transfer-Encoding: binary');
    header('Connection: Keep-Alive');
    header('Expires: 0');
    header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s T'));
    header('Pragma: public');
    echo $zip;

    return '';
}

This might be more of a basic PHP question, but does anyone know how I could do that?

2
  • Not sure what you're missing here – if $output is false, you're already returning the $zip, so the controller can output it. What is not working? As a sidenote, setting headers and outputting content should not happen in a utility method, it should happen in the controller.
    – MoritzLost
    Commented Jun 30, 2023 at 7:39
  • 1
    Thanks for the response. Whenever I see you respond to one of my questions I always get excited because I know it's going to be helpful! (I mean that sincerely) And indeed this is a good thing that I can't answer very well. I was just getting a "Safari can't download this file" error with the previous code. August's reply has helped push me in the right direction. Thanks again for the reply!
    – supazu
    Commented Jul 1, 2023 at 0:56

1 Answer 1

3

Assuming the Pass bundle is created in a service, your controller should only be responsible for calling that method and preparing a response. As @MoritzLost said, setting headers from a service method (or allowing another library to control the HTTP response) is generally inadvisable—in part because those tools exist in Craft/Yii, and in part because you should be able to create a Pass and ZIP file without caring about when or where it will be used (say, precompiling a bundle in a background job or building one to be attached to an email).

To that end, your service might look like this:

namespace mynamespace\myplugin\services;

use craft\base\Component;
use PKPass\PKPass;

class Passes extends Component
{
    public function createPass(): string
    {
        $pass = new PKPass('...', '...');
        $pass->setData([
            // ...
        ]);

        // Return the ZIP’s content, directly:
        return $pass->create(false);
    }
}

(This method could be parameterized so that it can look up an entry or whatever other info you need to create a unique pass!)

Your controller would look like this:

namespace mynamespace\myplugin\controllers;

use craft\web\Controller;
use mynamespace\myplugin\Plugin;

class PassController extends Controller
{
    public function actionGetPass(int $id)
    {
        // Call service to create pass:
        $pass = Plugin::getInstance()->getPasses()->createPass($someArg);

        // Send the file’s contents:
        return $this->response->sendContentAsFile($pass, 'Pass File Name.pkpass', [
            'mimeType' => 'application/vnd.apple.pkpass',
        ]);
    }
}

We're using the built-in sendContentAsFile() response method via the controller’s response property, and declaring the appropriate MIME type so that the client has a hint about what to do with the attachment.

Read more about sending files in the Yii documentation on handling requests!

3
  • Thanks August, this is great (and a good reminder of how much I still have to learn.) I think I'm close, but unsure on the $this->sendContentAsFile part. With that in place, I get a 'calling unknown method' error. Excuse my lack of knowledge, but how could I make that work?
    – supazu
    Commented Jul 1, 2023 at 0:45
  • Looks like I needed $this->response->sendContentAsFile() - got it working now! Thank you!!
    – supazu
    Commented Jul 2, 2023 at 6:05
  • 1
    Ah, shoot, you're totally right. Updating the answer accordingly! Commented Jul 3, 2023 at 15:06

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.