Summarized using AI

Beyond the Basics: Advanced Rails Techniques

Chris Oliver • September 05, 2025 • Amsterdam, Netherlands • Talk

Beyond the Basics: Advanced Rails Techniques

Speaker: Chris Oliver

Event: Rails World 2025

Introduction

Chris Oliver explores advanced Rails techniques beyond traditional best practices, focusing on customizing Rails applications to fit unique product domains. He emphasizes using the Rails framework in innovative ways, enhancing maintainability, readability, and developer experience.

Key Points

  • Rails Philosophy and Terminology

    • Rails' 'Railsy' feel comes from its readable, expressive nature, enabled largely by Ruby's flexibility.
    • Features like descriptive class methods (e.g., allow_unauthenticated_access, rate_limit) improve code clarity.
  • Creating Domain-Specific Class Methods

    • Suggests moving from before_action with private methods to expressive class methods (e.g., admin_access_only).
    • Class methods can accept arguments for customization (e.g., custom messages or redirection routes), adding flexibility over traditional hooks.
  • Extending the app/ Folder Structure

    • Encourages developers to organize domain concepts into new folders within the app/ directory (e.g., app/api_clients, app/components, app/notifiers, app/validators).
    • Case study: Hotwire course app uses a custom API client instead of relying on the full-featured Octokit gem, resulting in easier maintenance and fewer dependencies.
  • Custom Generators

    • Demonstrates how to build and leverage custom Rails generators tailored to project needs, such as generating API client stubs or overriding default scaffold templates with pagination or filtering.
  • Organizing Features by Concerns and Modules

    • Beyond code reuse, organize models by feature concerns within namespaced subfolders (e.g., billing under app/models/user/billing.rb).
    • Technique inspired by Campfire codebase, improving code organization, reducing file size, and clarifying feature boundaries.
  • Integrating Complex Features with Concerns and Validations

    • Example: Building a Turnstile CAPTCHA integration using concerns, custom validators, and an API client folder.
    • Shows the reusability and cleanliness achievable through such patterns, allowing validation logic to be transparently plugged into models.
  • Extending Rendering and Template Handling

    • Humorous yet technical example of adding PHP support as a renderer and template handler within Rails, paralleling the mechanism used by Jbuilder and Inertia gems.
    • Explains how template handlers work, drawing parallels to common gems and illustrating extensibility in Rails' view layer.
  • Advanced Controller Techniques

    • Discusses building a PDF rendering workflow using a gem called 'phen PDF' that utilizes pure Ruby (no Node.js) to drive headless Chrome, reflecting a commitment to seamless Rails integration and developer ergonomics.

Main Takeaways

  • Rails applications can be shaped around domain needs by extending and customizing folder structure, class methods, generators, and concerns.
  • Leveraging Ruby and Rails flexibility empowers expressive, maintainable code and deep framework integration.
  • Understanding and extending Rails internals (renderers, handlers, etc.) unlocks powerful capabilities for building advanced features tailored to the application's domain and developer workflows.

Beyond the Basics: Advanced Rails Techniques
Chris Oliver • Amsterdam, Netherlands • Talk

Date: September 05, 2025
Published: Mon, 15 Sep 2025 00:00:00 +0000
Announced: Tue, 20 May 2025 00:00:00 +0000

This talk showcases ways to take a traditional Rails app and shape it around your product domain—while also using Rails tools in ways you might not have considered.

Topics include:
- Creating your own app/ folders for domain concepts
- Creating your own generators
- Organizing features by modules and concerns (e.g., the User model may have teams, billing, etc., organized under app/models/user/)
- Adding domainwording and class methods, like allow_unauthenticated_access or rate_limit

Rails World 2025

00:00:06.960 So, I was trying to figure out what I
00:00:09.360 would talk about um or submit for a talk
00:00:12.160 at Rails World this year. And um a theme
00:00:15.839 I've kind of been obsessed about uh with
00:00:19.600 Rails has been like what makes Rails
00:00:22.160 feel so special. So, today we're going
00:00:24.000 to talk about uh beyond the basics and
00:00:26.240 getting into some advanced Rails
00:00:27.760 techniques. Um, I'm super excited to be
00:00:31.599 here again. Uh, last time in 2023 was
00:00:35.600 super special. So, it's it's great to be
00:00:37.840 back up on stage here. Um, so let's dive
00:00:40.559 in. What makes code feel Railsy? Uh,
00:00:44.160 it's kind of, you know, a nebulous
00:00:46.320 topic. Um, but I think David's talked
00:00:48.640 about this uh several times in the past
00:00:51.039 where like Ruby allows your code to feel
00:00:54.399 on equal footing as the code inside of
00:00:56.960 Ruby itself. active support can extend
00:01:00.800 uh integer class to say you know 7 days
00:01:04.239 and all kinds of cool things like that.
00:01:06.720 Um and terminology is a really important
00:01:10.400 piece of the Rails puzzle. Um when we
00:01:13.840 look at things like active record you
00:01:16.560 say has many subscribers has one
00:01:19.360 attached featured image has rich text
00:01:21.680 description validations are really
00:01:23.680 descriptive as well. Um, and that's one
00:01:26.960 of the things that is so special about
00:01:29.119 Rails. And the Ruby language allows that
00:01:31.600 to be possible. And this applies to our
00:01:34.720 controllers, too. So, we can have before
00:01:36.560 action authenticate user, and it just
00:01:39.680 does what it sounds like it's going to
00:01:41.439 do.
00:01:42.960 But, uh, with Rails 8 last year, I
00:01:46.159 thought, um, the allow unauthenticated
00:01:48.960 access caught my attention. I thought
00:01:50.720 that was pretty interesting. It kind of
00:01:52.640 does the opposite of devise where things
00:01:54.960 are uh locked down to registered users
00:01:58.240 or signed in users by default and you
00:02:00.640 break out of it. But instead of adding a
00:02:03.119 before action that you call, it has a
00:02:06.159 class method you call instead. So it
00:02:08.319 doesn't actually blend in with all of
00:02:09.920 the other before actions in your
00:02:12.000 controller, which you might have three
00:02:13.440 or four or five of them, and you have to
00:02:15.360 read the second word to actually
00:02:17.040 understand what is going on there. So
00:02:20.160 these stand out on purpose uh a bit and
00:02:23.520 the same applies to rate limiting. Rate
00:02:26.239 limiting reads extremely well. Rate
00:02:28.720 limit uh request to this controller to
00:02:31.200 10 within 5 minutes. I love it. But what
00:02:35.040 you might not realize is you can write
00:02:36.720 those class methods in your code too.
00:02:38.959 Typically we're not doing that, but we
00:02:41.280 absolutely can. So you might write
00:02:44.160 something like this where you have a
00:02:46.239 before action and you want to lock down
00:02:48.319 this controller to admins only and you
00:02:51.360 would say before action require admin.
00:02:53.360 You'd implement that logic in a private
00:02:56.000 method to require the admin.
00:02:59.360 But I would propose that you could use a
00:03:02.319 class method instead that stands out a
00:03:04.800 bit better and say admin access only and
00:03:08.159 have that be the focus. So instead of
00:03:10.959 implementing the logic for this in your
00:03:12.720 controller, we could extend this um and
00:03:16.560 put this in an authorization concern
00:03:19.280 that implements a class method that just
00:03:21.599 simply calls the before action right
00:03:24.159 there instead. So it's an extra little
00:03:26.480 layer over top of the before action, but
00:03:29.440 allows you to put better terminology
00:03:32.000 into your uh controllers and um and
00:03:36.000 emphasize the right stuff uh even more.
00:03:40.400 But there's another benefit to this with
00:03:42.720 class methods. They allow you to pass in
00:03:45.040 arguments. So if you went to build rate
00:03:48.080 limiting yourself, you might say before
00:03:50.400 action rate limit, but the symbol
00:03:52.720 doesn't allow you to pass in any
00:03:54.080 arguments. So if these two controllers
00:03:56.480 need different rate limiting um
00:03:59.439 settings, you'd have to build two
00:04:01.439 different rate limit methods in those
00:04:03.760 separate controllers.
00:04:05.760 But if we look at how Rails implements
00:04:08.400 rate limiting, it is a rate limit class
00:04:11.519 method. Uh you don't see the class
00:04:13.760 method part of the concern because
00:04:15.120 there's a lot of documentation in a
00:04:16.880 comment above. But you see that because
00:04:19.840 of Ruby um having the scope of this
00:04:23.919 before action and the lambda is within
00:04:25.919 the method, we can reference to and
00:04:28.800 within and the other options right
00:04:30.800 there. and it allows us to use that and
00:04:33.440 customize how it works.
00:04:36.240 So why don't we go back to our admin
00:04:38.800 access only method and uh refactor that
00:04:42.479 to take advantage of those arguments.
00:04:45.199 So we could say let's move the message
00:04:47.840 so you can customize it as an argument
00:04:50.160 there. We can keep the other options and
00:04:53.600 use that to pass along for example the
00:04:56.320 only or um you know if or unless or
00:05:00.000 whatever as options for before action
00:05:02.320 but we can pull out our specific
00:05:04.160 features um as options like message
00:05:07.600 here. So we could say now admin access
00:05:10.639 only get out or I'm calling the police
00:05:13.199 uh if somebody tries to access the admin
00:05:15.280 controller instead.
00:05:18.080 Um, this also is helpful if you maybe
00:05:22.320 want to uh redirect to a different route
00:05:25.600 depending on the situation. You could
00:05:27.680 pull out the route that is redirected to
00:05:30.479 as an argument too.
00:05:33.360 Another thing is you should not be
00:05:34.960 afraid to add your own folders to the
00:05:37.680 app folder in your Rails app. Here is an
00:05:41.520 example that has API clients, uh UI
00:05:45.199 components, admin resources, notifiers
00:05:48.240 for notifications, maybe uh
00:05:50.080 authorization policies like for pundit
00:05:53.120 um and active record validators. We can
00:05:55.759 add folders that are part of our
00:05:59.039 application and the domain it's trying
00:06:00.720 to solve and organize things there. You
00:06:03.840 don't have to shove things into the
00:06:05.520 existing folders. You have lots of
00:06:07.680 options uh to do that. But it feels kind
00:06:10.400 of like wrong maybe at first when you're
00:06:13.520 like it needs to just fit in one of the
00:06:15.360 buckets that Rails gives me. It does
00:06:17.440 not.
00:06:19.520 So here's an example. Uh in our learn
00:06:21.840 hotwire course application um we have
00:06:25.440 couple API clients uh and instead of
00:06:28.560 actually pulling in the gems for those
00:06:31.520 APIs, we have our own custom API client.
00:06:34.800 What's really nice about this is for
00:06:36.560 example with GitHub we you purchase the
00:06:40.000 course and you add your GitHub account
00:06:42.000 and we'll just invite you to the
00:06:44.720 organization so you can access the
00:06:46.560 source code and we could use the Octokit
00:06:49.280 gem for that but it implements every
00:06:52.000 single API endpoint that GitHub has. we
00:06:55.039 only need one and that gem updates
00:06:57.680 constantly because GitHub is changing a
00:06:59.680 lot of their APIs on a regular basis but
00:07:03.280 our code doesn't have a dependency on
00:07:05.840 anything else no third party
00:07:07.199 dependencies and we only have to
00:07:08.720 implement one thing and that's not going
00:07:10.639 to change very often so this is way way
00:07:14.000 more maintainable in the long run which
00:07:16.400 is nice
00:07:20.080 um we can also build our own custom
00:07:22.080 generators so Rails ships with a bunch
00:07:24.400 of generators. You might use a gem that
00:07:26.319 has generators. You can put generators
00:07:28.479 in your own application too. So of
00:07:31.039 course we can make a generator for API
00:07:33.840 clients and we can run rails generate
00:07:36.160 generator for API client to generate the
00:07:38.639 generator. Um, and this is super useful
00:07:44.000 because out of the box it's going to
00:07:45.840 inherit from uh the named base generator
00:07:49.440 class. And this is responsible for
00:07:52.160 handling that first argument when you
00:07:54.479 run like rails generate model or
00:07:56.639 migration.
00:07:58.160 You give that first argument the name of
00:08:00.160 the model and it includes a bunch of
00:08:02.720 helpers on the file path for it. Um, you
00:08:06.879 know, all kinds of different things. And
00:08:08.319 I'll show you uh how we can use that
00:08:10.800 here.
00:08:14.960 So our implementation for building um
00:08:18.240 the generator can simply say here's
00:08:20.879 where you can find all the templates. Um
00:08:23.280 we can add like options as well. So you
00:08:25.599 can say d- URL and specify the base URL
00:08:29.199 um for the API like api.github.com
00:08:31.120 github.com
00:08:32.640 and then any public methods in here will
00:08:35.360 be executed and they will take the
00:08:38.640 template out of the um that directory
00:08:42.240 and then copy it into the application at
00:08:44.880 the using like the file path helper
00:08:47.760 which will also handle things like
00:08:49.839 namespacing automatically for you and
00:08:52.240 that makes this really really easy to uh
00:08:54.880 to build your own tools in your
00:08:57.120 application which is nice.
00:08:59.760 And speaking of generators, you can
00:09:01.600 override templates from any generator uh
00:09:04.959 in Rails or gems as well.
00:09:09.120 So for example, um scaffolds typically
00:09:12.240 have the notice at the top for every
00:09:14.560 show action which you probably have
00:09:16.959 extracted into your application layout
00:09:19.440 and have one single place for notices.
00:09:21.440 So this would end up generating notices
00:09:23.440 twice and displaying them twice. So you
00:09:25.680 can just override this file under lib
00:09:27.839 templates erb scaffold show.html.erb.tt
00:09:32.800 just delete that line and now your
00:09:34.640 scaffolds won't do that. Um and you can
00:09:37.120 take this further for example uh we will
00:09:39.920 take like the index and we'll add
00:09:42.240 pagionation to it or filtering and s uh
00:09:45.360 sorting and search. um you can do all
00:09:47.920 kinds of stuff with it and then actually
00:09:50.160 rather than just like saying ah we don't
00:09:52.320 use scaffolds anymore, you've made them
00:09:54.480 work for your team and for your
00:09:55.920 application and then it'll be all
00:09:57.440 consistent and you can keep using those
00:09:59.200 features.
00:10:01.440 Um let's also talk about concerns. Uh
00:10:04.399 typically when we think about concerns,
00:10:06.959 think about reusable code. Um, so you
00:10:10.240 might make a sortable module and it adds
00:10:13.600 a couple sorting functions and this can
00:10:15.440 be used in literally any model in your
00:10:17.680 Rails app. But
00:10:20.320 concerns can be used for organizing
00:10:22.560 features. Um, David open sourced
00:10:25.360 Campfire and one of the things that
00:10:27.519 caught my attention in there was they
00:10:30.000 organized parts of their models by
00:10:33.440 feature using concerns and rather than
00:10:36.399 putting them in the concerns folder,
00:10:38.560 they'll put them in the name space. So
00:10:42.000 when we include billing, it will first
00:10:44.399 check to see if there's a user billing
00:10:46.320 module or it will fall back to looking
00:10:48.800 in the top level namespace. Same with
00:10:51.839 mentions. So we can go implement all of
00:10:54.320 our billing code under user billing. And
00:10:57.360 this is going to live in the
00:11:01.839 app models user folder instead of app
00:11:05.680 models concerns because it's not reused
00:11:07.839 by anybody else. But it can have all of
00:11:10.000 our building code in one place which
00:11:13.120 tends to get messy if you have it all in
00:11:15.760 one file where you might have functions
00:11:18.240 and scopes and constants or whatever in
00:11:21.519 various places in the file. You can put
00:11:24.079 it all in one place now and it doesn't
00:11:26.399 mix together with the other uh features
00:11:29.120 in there. Same thing goes for like
00:11:32.000 implementing mentions, including the
00:11:34.160 attachable module, and then maybe
00:11:36.240 setting the uh the text representation
00:11:39.600 with the user's name as a default if
00:11:42.079 there wasn't a caption. And you might
00:11:44.240 want a very a different one of these. If
00:11:46.880 you have say blog posts with titles, you
00:11:49.839 could do the same thing, but change the
00:11:51.600 implementation of the text
00:11:54.079 representation.
00:11:56.320 So let's take a look at an example um
00:11:59.519 where uh we'll take and use a bunch of
00:12:02.880 th these techniques and build a
00:12:05.839 cloudflare turnstyle integration.
00:12:09.279 So you might do this as your first
00:12:12.000 implementation where we need to verify a
00:12:14.560 turn style before we create the user
00:12:16.480 record. Um and there's a couple issues
00:12:18.959 with this. Uh so first it's kind of made
00:12:22.240 your controller a bit me more messy but
00:12:25.839 also if turnstyle validation fails we
00:12:28.959 haven't even attempted to validate the
00:12:30.800 user. So they would fill in their stuff
00:12:34.399 for turnstyle that would rerender the
00:12:36.800 form with an error but we wouldn't
00:12:38.560 actually check and show any errors for
00:12:40.560 the user's email or password or
00:12:42.720 whatever. So then they might have to
00:12:44.639 fill out turn style like three or four
00:12:46.480 times and it would be annoying and it
00:12:47.839 would be nice if it was all integrated
00:12:49.760 into the same flow.
00:12:52.480 So what if we made that a concern? This
00:12:55.040 in this case would be a reusable concern
00:12:57.600 because we could use this for any model.
00:12:59.920 So we can make uh challenges turn style
00:13:04.079 and the implementation for that would
00:13:05.839 look something like this where we make a
00:13:07.440 module adds a virtual attribute for the
00:13:09.839 challenge token and a validation. But
00:13:12.800 we're going to make up a custom
00:13:14.399 validation called turnstyle and set it
00:13:16.720 to true to run that. Rails will look for
00:13:20.160 a class called turnstyle validator
00:13:23.839 and we can implement that um in our own
00:13:27.680 custom folder for app validators and
00:13:30.720 then it's going to be given the record
00:13:33.440 the attribute and the value of it which
00:13:36.000 we can send over to the turn style API
00:13:38.959 and ask the JSON response for success
00:13:42.079 was it true or false if it was not
00:13:44.320 successful we can add a record or an
00:13:46.720 error to the record um and display that
00:13:50.000 in uh the form.
00:13:53.040 So then of course we can use our
00:13:55.440 generated generator for the generator of
00:13:58.320 API clients uh and create that and we
00:14:01.360 just call it turnstyle that's going to
00:14:03.600 then create this file which we can add
00:14:06.639 the base URI to the turnstyle API. We
00:14:10.399 have the secret we can look up from
00:14:12.399 either environment variables or Rails
00:14:14.560 credentials. Then we just need to make a
00:14:16.800 post request to the endpoint. Give it
00:14:19.600 our secret, our token, and the optional
00:14:22.160 IP address, which if we use current
00:14:24.639 attributes, we don't have to also pass
00:14:26.880 that along to the model. We can just
00:14:28.720 reference that after we save it in the
00:14:31.279 controller in a before action.
00:14:34.800 So then this allows us to come back to
00:14:36.720 our controller and go back to a very
00:14:39.279 simple implementation. We don't have to
00:14:41.680 have extra methods in here or anything.
00:14:43.920 It's now handled automatically by active
00:14:46.160 records validations. The only change we
00:14:48.560 would do here is our signup params would
00:14:51.199 need to um permit the challenge token on
00:14:55.120 that user. Um so we just accept another
00:14:58.079 parameter just like normal um which is
00:15:00.399 easy and fits the entire pattern of
00:15:02.720 everything else. So turnstyle now feels
00:15:05.600 like assigning any other attribute um
00:15:08.079 which is great. And then of course you
00:15:10.959 can extract this out as a plugin for
00:15:13.120 rails.
00:15:14.800 So we can take that concern and we can
00:15:17.519 make it a little bit more mod but
00:15:19.120 modular by adding a challenges with
00:15:22.160 class method that takes the service
00:15:24.240 name. So then we can support turnstyle
00:15:27.279 recapture h capture and all our um rails
00:15:31.279 plugin would need to do is add those
00:15:32.959 extra validators and you're off to the
00:15:35.440 races. Um, and then we can use active
00:15:37.760 support onload, active record in order
00:15:40.480 to uh make that added to rails whenever
00:15:44.399 it boots and is required and loaded.
00:15:48.800 Speaking of extending Rails,
00:15:51.600 um, a couple weeks ago, there was this
00:15:53.600 post on Reddit. Uh, 70 companies you
00:15:56.560 didn't know were using Rails in 2025.
00:15:59.279 The first comment on there was, "Go
00:16:00.880 Rails." What a surprise. And I was like,
00:16:04.160 you know what? Imagine the scandal if we
00:16:06.720 were using WordPress. Uh, that would be
00:16:08.560 pretty funny. So, I got looking at the
00:16:11.199 Go Rails admin dashboard and it got me
00:16:14.000 thinking,
00:16:16.000 you know, what if we took this joke too
00:16:18.320 far? Uh, and of course, Tenderlove like
00:16:22.000 16 years ago made Fuby, which some of
00:16:24.240 you guys are familiar with, that
00:16:26.079 actually is a Ruby gem that compiles PHP
00:16:30.399 and a C extension to talk to PHP from
00:16:33.040 Ruby. Um, I didn't bother updating that
00:16:36.560 to work with the latest version of PHP,
00:16:39.040 but I did wonder what if we could just
00:16:41.040 render PHP right from your controllers.
00:16:44.399 So, obviously,
00:16:46.320 brew install PHP. Let's make this
00:16:49.279 actually work because why not? Um, and
00:16:52.480 you'd be surprised. It's these four
00:16:54.639 lines of code. Uh, you add a renderer,
00:16:57.680 you register PHP as a renderer. the
00:17:01.120 options you saw in the render call, the
00:17:03.920 ones for PHP will be put in the uh first
00:17:06.640 argument there, the PHP code. The rest
00:17:09.120 of it is the second argument, which we
00:17:11.039 don't care about. And then we just tell
00:17:13.039 Rails uh we're going to return HTML in
00:17:15.679 the response. And then we'll shell out
00:17:17.679 to the PHP um command line. Just give it
00:17:20.720 that PHP code. Uh and we can open this
00:17:23.360 up in our browser. And hello world from
00:17:25.600 PHP works, which is awesome. Uh, and if
00:17:28.720 you don't believe me, we can try PHP
00:17:30.880 info. And you will see that it's running
00:17:33.520 the latest version of PHP 8.4 on my uh
00:17:37.360 on my Mac here installed with Homebrew,
00:17:40.559 which is great. Uh, but obviously we got
00:17:44.320 to take this joke too far. So, we need
00:17:46.160 to go deeper. So, why not support PHP
00:17:50.080 templates in Rails?
00:17:53.039 So, in order to do that, it's helpful to
00:17:55.039 understand the uh syntax of the file
00:17:57.200 name. So we have index which is the
00:17:58.799 action name. HTML is the format and ERB
00:18:02.240 is actually the template handler like
00:18:04.160 the processor of that.
00:18:06.960 So we need obviously index.php.php
00:18:12.880 and we can register that stuff and and
00:18:15.360 make it work of course. So in order to
00:18:18.400 tell Rails that we're looking for the
00:18:19.919 PHP format we add PHP that's normally
00:18:23.520 HTML of course. Um so we do that. We try
00:18:27.120 and access that, but the Rails logs say,
00:18:30.480 "The hell are you talking about?" You
00:18:32.480 know, we don't know what that is. So,
00:18:35.440 one line of code, you say, you know, PHP
00:18:38.000 is just an alias for for HTML content.
00:18:42.240 And, uh, yeah, Rails now understands
00:18:44.480 PHP.
00:18:46.000 Um, so this is good. It gets us to the
00:18:49.120 point where we can start processing
00:18:52.320 those files. So we can register a
00:18:55.039 template handler for PHP ERB files. And
00:18:59.039 then it needs um a callable object. It
00:19:02.240 could be a lambda. It could be a class
00:19:03.760 with the call method on it that'll give
00:19:05.679 us the template and it will give us the
00:19:07.440 source. And then what do we do? Um we
00:19:10.160 need to process it. But how does this
00:19:11.760 stuff work? Well, if we throw a binding
00:19:15.440 IRB in there, we can look at this
00:19:17.039 template variable and call identifier.
00:19:19.520 And you'll see this is the full path uh
00:19:21.919 to that file on disk. You know, my I
00:19:24.799 love PHP user on my Mac, uh PHP on Rails
00:19:28.400 app and so on. Um and if we poke around
00:19:32.559 inside the Rails source code, you'll see
00:19:35.200 uh the other handlers in there. HTML is
00:19:37.600 one of them. But this is kind of weird,
00:19:39.760 right? Like uh on line 7, instead of
00:19:42.559 returning Ruby code, it's returning a
00:19:44.880 string of Ruby codes. That's that seems
00:19:47.919 weird, but what actually is happening
00:19:49.919 here is this is how Ruby or Rails
00:19:52.640 compiles your templates into your Rails
00:19:55.760 app. So, what it's doing is you're
00:19:59.039 returning PHP or Ruby code in this case.
00:20:03.280 Um, and it's going to take that and
00:20:04.720 define a method so that instead of
00:20:07.120 having to read from disk and evaluate
00:20:08.880 the ERB file every time, it's going to
00:20:10.960 evaluate it and then define a method
00:20:12.960 that it can just call much quicker. So,
00:20:15.760 we need to go back to our template
00:20:18.000 handler and then change it to return a
00:20:20.559 string of Ruby code. We'll shell out to
00:20:23.039 PHP to evaluate the file. We can mark it
00:20:25.679 as HTML safe so it trusts the content
00:20:27.919 for it um or whatever and then return
00:20:30.880 that back.
00:20:32.720 So, let's test it out. We'll make an
00:20:35.360 index.php PHP ERB file. We'll put some
00:20:38.880 PHP tag to print out a paragraph and a
00:20:41.120 hello world and uh some ERB for good
00:20:44.080 measure because we need to make sure
00:20:45.679 that still works and we can include
00:20:47.600 those Ruby things in there. And we try
00:20:51.360 it and it works. Um sort of the hello
00:20:55.440 world gets printed out. There's a space
00:20:57.039 in between so we know our paragraph tag
00:20:59.120 is actually being rendered correctly. Um
00:21:02.400 but we still need to handle the ERB
00:21:04.159 portion of this. So um we can come back
00:21:08.799 to our method uh for the handler and
00:21:12.080 we'll capture the output from PHP and
00:21:15.039 we'll just forward along to the regular
00:21:16.640 ERB processor. So we'll give it the
00:21:19.120 template but the modified source code
00:21:21.600 from PHP and voila
00:21:26.159 we have uh the params being printed out
00:21:28.640 now as well. So now we have PHP and ERB
00:21:32.240 working perfectly together uh which is
00:21:34.400 great. But of course, we need to make
00:21:36.240 sure this works with partials and
00:21:38.000 layouts and stuff, too. So, why not make
00:21:40.080 a layout that prints out a copyright in
00:21:42.960 the footer, but we'll use PHP to to
00:21:45.280 render the date out there. And there we
00:21:48.159 go. So, that's great,
00:21:52.559 but why, you know, why are we doing
00:21:55.120 this? Um, oh, I meant to delete this
00:21:58.000 slide.
00:21:59.360 Uh
00:22:02.480 so
00:22:04.159 this is exactly how things work in
00:22:07.039 common gems and everything. So
00:22:08.640 index.json.jbuilder
00:22:10.960 same exact concept here. Um and the
00:22:13.679 Jbuilder gem if you open up the rail tie
00:22:16.320 has an onload hook for action view and
00:22:19.120 it says hey let's register the Jbuilder
00:22:21.440 template handler and we'll uh process
00:22:24.960 that with the Jbuilder handler class and
00:22:28.880 um it actually also overrides the Rails
00:22:31.919 templates. Um, so Rails itself doesn't
00:22:35.520 actually this the scaffolding and stuff
00:22:37.520 in Rails doesn't actually support JSON,
00:22:40.320 but that is added by Jbuilder. Jbuilder
00:22:43.120 is a default gem in your gem file. So
00:22:45.280 that's why you always see your scaffolds
00:22:47.200 create the HTML responses plus the JSON,
00:22:50.640 but it just overrides those templates as
00:22:52.799 well. Um, and it turns out as I was
00:22:56.159 looking at this, apparently I was the
00:22:58.000 last commit in this folder on Jbuilder,
00:23:00.640 which was uh funny. Um, but if you've
00:23:04.080 looked at inertia rails, this is exactly
00:23:07.679 how it works. So, render inertia, give
00:23:10.640 it a component name for reactor view um
00:23:13.679 in your props, and it's able to take
00:23:15.440 your props, convert them to JSON, and
00:23:17.679 serialize them and pass them over to
00:23:20.640 your your reactor view components. Uh,
00:23:23.919 and if you poke around their gem, same
00:23:26.320 exact thing. adds a renderer to action
00:23:28.880 controller for inertia and then hand
00:23:31.120 hands it off to be processed to create
00:23:33.440 the response. Um, and a lot of this I
00:23:37.440 was working on a gem recently called
00:23:39.440 phen PDF uh where I wanted to generate
00:23:42.400 PDFs using HTML and CSS. Um, but WKHTML
00:23:48.000 to PDF is a common gem that we use for
00:23:50.880 that or have used but it's now
00:23:52.720 deprecated. Uh there's Grover, which is
00:23:55.520 great, and it talks to Chrome, like
00:23:57.919 headless Chrome, using Puppeteer to
00:24:00.960 export uh PDFs out of Chrome. The
00:24:04.000 annoying part about that is it requires
00:24:06.080 uh Node.js to be installed to have Rails
00:24:10.080 talk to Node to talk to Chrome to talk
00:24:12.400 back and it's kind of uh convoluted.
00:24:16.080 And I wanted no NodeJS. There's no in
00:24:18.799 the name for a reason. Um, and so Phum
00:24:22.960 is a a gem that you may be familiar
00:24:25.440 with, but it basically implements
00:24:27.039 Puppeteer um using pure Ruby. So, it
00:24:29.919 implements the CDP protocol for Chrome.
00:24:32.640 And I was like, you know, this seems
00:24:34.240 like a good idea uh in order to build a
00:24:37.440 nodeless version of exporting PDFs and
00:24:40.080 Rails. And what I wanted to do was this.
00:24:44.159 I wanted to be able to visit a route and
00:24:47.120 have it render HTML, but then also
00:24:50.640 capture the HTML, boot up headless
00:24:52.799 Chrome, insert it into a new tab, export
00:24:56.000 the tab as a PDF, capture the PDF, and
00:24:58.799 then send it back to the browser. So,
00:25:00.480 when you make a request to one of these
00:25:02.720 routes, we end up kind of double
00:25:04.640 rendering. Um, but we render the HTML
00:25:07.600 just so that we can do that. And then
00:25:09.279 what we really want to send back to the
00:25:10.880 user is the PDF. So, uh, as I was
00:25:14.480 working on this, what I was like, you
00:25:16.240 know, how do I do this cleanly in the
00:25:19.039 controller and make it really fit into
00:25:20.960 Rails? Uh, and I came up with this,
00:25:23.279 which led me into all these nonsense
00:25:26.159 things like, uh, render PHP. Um, but
00:25:29.919 this is how the gem works now. You can
00:25:31.760 just simply say render farm PDF. It will
00:25:34.480 actually effectively call the format
00:25:36.480 HTML behind the scenes. And then you can
00:25:39.840 pass in options with the curly braces um
00:25:42.559 for PDF specs of like maybe you want it
00:25:45.360 to be landscape mode or whatever. But
00:25:48.080 then it gets the PDF back from Chrome
00:25:51.360 and we can take the extra options like
00:25:53.520 disposition and file name and use those
00:25:55.840 with the send data method to give the
00:25:58.240 hints to the browser that yes uh we're
00:26:00.480 giving you a PDF but we'd like you to
00:26:02.240 display it inline and here's a suggested
00:26:04.640 file name for it. So it ends up being uh
00:26:08.240 really elegant and just kind of fits
00:26:09.760 with Rails and you don't even have to
00:26:11.360 worry too much about calling this mod
00:26:14.400 like the class from the gem or anything
00:26:16.799 like that. It's just kind of seamless.
00:26:20.559 So that's about it. Um but obviously
00:26:24.400 Marco's been working on Herb making ERB
00:26:27.919 very HTML aware. So I'm going to need
00:26:29.919 you guys to help me convince him to add
00:26:31.520 PHP support to it. So, uh, not like he
00:26:35.679 needs anything more to work on or
00:26:37.440 anything, but, uh, I feel like we need
00:26:39.760 that now. Um, so yeah, thanks
Explore all talks recorded at Rails World 2025
+19